Merge pull request #353 from AyuntamientoMadrid/hot-score-347

Hot score 347
This commit is contained in:
Raimond Garcia
2015-09-04 17:24:42 +02:00
21 changed files with 311 additions and 182 deletions

View File

@@ -5,8 +5,7 @@ module HasFilters
def has_filters(valid_filters, *args)
before_action(*args) do
@valid_filters = valid_filters
@current_filter = params[:filter]
@current_filter = @valid_filters.first unless @valid_filters.include?(@current_filter)
@current_filter = @valid_filters.include?(params[:filter]) ? params[:filter] : @valid_filters.first
end
end
end

View File

@@ -82,8 +82,8 @@ class DebatesController < ApplicationController
end
def parse_order
@valid_orders = ['created_at', 'score', 'most_commented', 'random']
@order = @valid_orders.include?(params[:order]) ? params[:order] : 'created_at'
@valid_orders = ['hot_score', 'created_at', 'score', 'most_commented', 'random']
@order = @valid_orders.include?(params[:order]) ? params[:order] : @valid_orders.first
end
def parse_tag_filter

View File

@@ -27,7 +27,7 @@ class Moderation::CommentsController < Moderation::BaseController
private
def load_comments
@comments = Comment.accessible_by(current_ability, :hide).flagged.sorted_for_moderation.includes(:commentable)
@comments = Comment.accessible_by(current_ability, :hide).flagged.sort_for_moderation.includes(:commentable)
end
end

View File

@@ -26,7 +26,7 @@ class Moderation::DebatesController < Moderation::BaseController
private
def load_debates
@debates = Debate.accessible_by(current_ability, :hide).flagged.sorted_for_moderation
@debates = Debate.accessible_by(current_ability, :hide).flagged.sort_for_moderation
end
end

View File

@@ -2,8 +2,8 @@ class WelcomeController < ApplicationController
skip_authorization_check
def index
@featured_debates = Debate.includes(:tags).limit(3)
@featured_debates = Debate.sort_by_hot_score.limit(3).for_render
set_debate_votes(@featured_debates)
end
end
end

View File

@@ -17,13 +17,15 @@ class Comment < ActiveRecord::Base
scope :recent, -> { order(id: :desc) }
scope :sorted_for_moderation, -> { order(flags_count: :desc, updated_at: :desc) }
scope :sort_for_moderation, -> { order(flags_count: :desc, updated_at: :desc) }
scope :pending_flag_review, -> { where(ignored_flag_at: nil, hidden_at: nil) }
scope :with_ignored_flag, -> { where("ignored_flag_at IS NOT NULL AND hidden_at IS NULL") }
scope :flagged, -> { where("flags_count > 0") }
scope :for_render, -> { with_hidden.includes(user: :organization) }
after_create :call_after_commented
def self.build(commentable, user, body, p_id=nil)
new commentable: commentable,
user_id: user.id,
@@ -87,4 +89,8 @@ class Comment < ActiveRecord::Base
!root?
end
def call_after_commented
self.commentable.try(:after_commented)
end
end

View File

@@ -1,7 +1,5 @@
require 'numeric'
class Debate < ActiveRecord::Base
default_scope { order(created_at: :desc) }
apply_simple_captcha
TITLE_LENGTH = Debate.columns.find { |c| c.name == 'title' }.limit
@@ -23,15 +21,18 @@ class Debate < ActiveRecord::Base
before_validation :sanitize_description
before_validation :sanitize_tag_list
scope :sorted_for_moderation, -> { order(flags_count: :desc, updated_at: :desc) }
before_save :calculate_hot_score
scope :sort_for_moderation, -> { order(flags_count: :desc, updated_at: :desc) }
scope :pending_flag_review, -> { where(ignored_flag_at: nil, hidden_at: nil) }
scope :with_ignored_flag, -> { where("ignored_flag_at IS NOT NULL AND hidden_at IS NULL") }
scope :flagged, -> { where("flags_count > 0") }
scope :for_render, -> { includes(:tags) }
scope :sort_by_score , -> { reorder(cached_votes_score: :desc) }
scope :sort_by_created_at, -> { reorder(created_at: :desc) }
scope :sort_by_most_commented, -> { reorder(comments_count: :desc) }
scope :sort_by_random, -> { reorder("RANDOM()") }
scope :sort_by_hot_score , -> { order(hot_score: :desc) }
scope :sort_by_score , -> { order(cached_votes_score: :desc) }
scope :sort_by_created_at, -> { order(created_at: :desc) }
scope :sort_by_most_commented, -> { order(comments_count: :desc) }
scope :sort_by_random, -> { order("RANDOM()") }
# Ahoy setup
visitable # Ahoy will automatically assign visit_id on create
@@ -103,6 +104,30 @@ class Debate < ActiveRecord::Base
update(ignored_flag_at: Time.now)
end
def after_commented
save # updates teh hot_score because there is a before_save
end
def calculate_hot_score
z = 1.96 # Normal distribution with a confidence of 0.95
time_unit = 1.0 * 12.hours
start = Time.new(2015, 6, 15)
comments_weight = 1.0/3 # 3 comments == 1 positive vote
weighted_score = 0
n = cached_votes_total + comments_weight * comments_count
if n > 0 then
pos = cached_votes_up + comments_weight * comments_count
phat = 1.0 * pos / n
weighted_score = (phat + z*z/(2*n) - z * Math.sqrt((phat*(1-phat)+z*z/(4*n))/n))/(1+z*z/n)
end
age_in_units = 1.0 * ((created_at || Time.now) - start) / time_unit
self.hot_score = (age_in_units**3 + weighted_score * 1000).round
end
protected
def sanitize_description
@@ -112,5 +137,4 @@ class Debate < ActiveRecord::Base
def sanitize_tag_list
self.tag_list = TagSanitizer.new.sanitize_tag_list(self.tag_list)
end
end

View File

@@ -19,7 +19,7 @@
<table>
<% @organizations.each do |organization| %>
<tr>
<tr id="<%= dom_id(organization) %>">
<td><%= organization.name %></td>
<td><%= organization.email %></td>
<td><%= organization.phone_number %></td>

View File

@@ -11,6 +11,8 @@
<p class="debate-info">
<i class="icon-comments"></i>&nbsp;
<%= link_to t("debates.debate.comments", count: debate.comments_count), debate_path(debate, anchor: "comments") %>
<span class="bullet">&nbsp;&bullet;&nbsp;</span>
<%= l debate.created_at.to_date %>
</p>
<div class="debate-description">
<%= link_to debate.description, debate %>

View File

@@ -69,6 +69,7 @@ en:
select_order: Order by
select_order_long: Order debates by
orders:
hot_score: most active
created_at: newest
score: best rated
most_commented: most commented

View File

@@ -69,6 +69,7 @@ es:
select_order: Ordenar por
select_order_long: Estás viendo los debates
orders:
hot_score: "más activos"
created_at: "más nuevos"
score: "mejor valorados"
most_commented: "más comentados"

View File

@@ -59,6 +59,7 @@ tags = Faker::Lorem.words(25)
description = "<p>#{Faker::Lorem.paragraphs.join('</p></p>')}</p>"
debate = Debate.create!(author: author,
title: Faker::Lorem.sentence(3),
created_at: rand((Time.now - 1.week) .. Time.now),
description: description,
tag_list: tags.sample(3).join(','),
terms_of_service: "1")
@@ -72,6 +73,7 @@ puts "Commenting Debates"
author = User.reorder("RANDOM()").first
debate = Debate.reorder("RANDOM()").first
Comment.create!(user: author,
created_at: rand(debate.created_at .. Time.now),
commentable: debate,
body: Faker::Lorem.sentence)
end
@@ -82,7 +84,8 @@ puts "Commenting Comments"
(1..100).each do |i|
author = User.reorder("RANDOM()").first
parent = Comment.reorder("RANDOM()").first
comment = Comment.create!(user: author,
Comment.create!(user: author,
created_at: rand(parent.created_at .. Time.now),
commentable_id: parent.commentable_id,
commentable_type: parent.commentable_type,
body: Faker::Lorem.sentence,

View File

@@ -0,0 +1,5 @@
class AddHotScoreToDebates < ActiveRecord::Migration
def change
add_column :debates, :hot_score, :bigint, default: 0
end
end

View File

@@ -101,6 +101,7 @@ ActiveRecord::Schema.define(version: 20150903200440) do
t.datetime "confirmed_hide_at"
t.integer "cached_anonymous_votes_total", default: 0
t.integer "cached_votes_score", default: 0
t.integer "hot_score", limit: 8, default: 0
end
add_index "debates", ["cached_votes_down"], name: "index_debates_on_cached_votes_down", using: :btree

9
lib/tasks/debates.rake Normal file
View File

@@ -0,0 +1,9 @@
namespace :debates do
desc "Updates all debates by recalculating their hot_score"
task hot_score: :environment do
Debate.find_in_batches do |debates|
debates.each(&:save)
end
end
end

View File

@@ -71,6 +71,10 @@ FactoryGirl.define do
Flag.flag(FactoryGirl.create(:user), debate)
end
end
trait :with_hot_score do
before(:save) { |d| d.calculate_hot_score }
end
end
factory :vote do

View File

@@ -13,7 +13,7 @@ feature 'Admin::Organizations' do
background do
@user = create(:user, email: "marley@humanrights.com", phone_number: "6764440002")
organization = create(:organization, user: @user, name: "Get up, Stand up")
create(:organization, user: @user, name: "Get up, Stand up")
end
scenario "returns no results if search term is empty" do
@@ -70,11 +70,13 @@ feature 'Admin::Organizations' do
organization = create(:organization)
visit admin_organizations_path
expect(current_path).to eq(admin_organizations_path)
expect(page).to have_link('Verify')
expect(page).to have_link('Reject')
within("#organization_#{organization.id}") do
expect(current_path).to eq(admin_organizations_path)
expect(page).to have_link('Verify')
expect(page).to have_link('Reject')
click_on 'Verify'
click_on 'Verify'
end
expect(current_path).to eq(admin_organizations_path)
expect(page).to have_content ('Verified')
@@ -86,11 +88,13 @@ feature 'Admin::Organizations' do
visit admin_organizations_path
expect(current_path).to eq(admin_organizations_path)
expect(page).to have_content ('Verified')
expect(page).to_not have_link('Verify')
expect(page).to have_link('Reject')
within("#organization_#{organization.id}") do
expect(page).to have_content ('Verified')
expect(page).to_not have_link('Verify')
expect(page).to have_link('Reject')
click_on 'Reject'
click_on 'Reject'
end
expect(current_path).to eq(admin_organizations_path)
expect(page).to have_content ('Rejected')
@@ -102,10 +106,12 @@ feature 'Admin::Organizations' do
visit admin_organizations_path
expect(current_path).to eq(admin_organizations_path)
expect(page).to have_link('Verify')
expect(page).to_not have_link('Reject', exact: true)
within("#organization_#{organization.id}") do
expect(page).to have_link('Verify')
expect(page).to_not have_link('Reject', exact: true)
click_on 'Verify'
click_on 'Verify'
end
expect(current_path).to eq(admin_organizations_path)
expect(page).to have_content ('Verified')

View File

@@ -364,80 +364,87 @@ feature 'Debates' do
expect(Flag.flagged?(user, debate)).to_not be
end
feature 'Debate index order filters', :js do
feature 'Debate index order filters' do
before do
@most_commented_debate = create(:debate)
@most_score_debate = create(:debate)
@most_recent_debate = create(:debate)
create_list(:comment, 2, commentable: @most_commented_debate)
create_list(:vote, 2, votable: @most_score_debate)
create_list(:vote, 2, votable: @most_recent_debate, vote_flag: false)
create(:vote, votable: @most_recent_debate)
create(:comment, commentable: @most_recent_debate)
end
scenario 'Default order is hot_score', :js do
create(:debate, title: 'best').update_column(:hot_score, 10)
create(:debate, title: 'worst').update_column(:hot_score, 2)
create(:debate, title: 'medium').update_column(:hot_score, 5)
scenario 'Default order is created_at' do
visit debates_path
expect(page).to have_select('order-selector', selected: 'newest')
expect(@most_recent_debate.title).to appear_before(@most_score_debate.title)
expect(@most_score_debate.title).to appear_before(@most_commented_debate.title)
expect(page).to have_select('order-selector', selected: 'most active')
expect('best').to appear_before('medium')
expect('medium').to appear_before('worst')
end
scenario 'Debates are ordered by best rated' do
visit debates_path
scenario 'Debates are ordered by best rated', :js do
create(:debate, title: 'best', cached_votes_score: 10)
create(:debate, title: 'medium', cached_votes_score: 5)
create(:debate, title: 'worst', cached_votes_score: 2)
visit debates_path
select 'best rated', from: 'order-selector'
expect(page).to have_select('order-selector', selected: 'best rated')
expect(find("#debates .debate", match: :first)).to have_content(@most_score_debate.title)
within '#debates' do
expect('best').to appear_before('medium')
expect('medium').to appear_before('worst')
end
expect(current_url).to include('order=score')
expect(@most_score_debate.title).to appear_before(@most_commented_debate.title)
expect(@most_commented_debate.title).to appear_before(@most_recent_debate.title)
end
scenario 'Debates are ordered by most commented' do
visit debates_path
scenario 'Debates are ordered by most commented', :js do
create(:debate, title: 'best', comments_count: 10)
create(:debate, title: 'medium', comments_count: 5)
create(:debate, title: 'worst', comments_count: 2)
visit debates_path
select 'most commented', from: 'order-selector'
expect(page).to have_select('order-selector', selected: 'most commented')
expect(find("#debates .debate", match: :first)).to have_content(@most_commented_debate.title)
within '#debates' do
expect('best').to appear_before('medium')
expect('medium').to appear_before('worst')
end
expect(current_url).to include('order=most_commented')
expect(@most_commented_debate.title).to appear_before(@most_recent_debate.title)
expect(@most_recent_debate.title).to appear_before(@most_score_debate.title)
end
scenario 'Debates are ordered by newest' do
scenario 'Debates are ordered by newest', :js do
create(:debate, title: 'best', created_at: Time.now)
create(:debate, title: 'medium', created_at: Time.now - 1.hour)
create(:debate, title: 'worst', created_at: Time.now - 1.day)
visit debates_path
select 'best rated', from: 'order-selector'
expect(find("#debates .debate", match: :first)).to have_content(@most_score_debate.title)
select 'newest', from: 'order-selector'
expect(page).to have_select('order-selector', selected: 'newest')
expect(find("#debates .debate", match: :first)).to have_content(@most_recent_debate.title)
within '#debates' do
expect('best').to appear_before('medium')
expect('medium').to appear_before('worst')
end
expect(current_url).to include('order=created_at')
expect(@most_recent_debate.title).to appear_before(@most_score_debate.title)
expect(@most_score_debate.title).to appear_before(@most_commented_debate.title)
end
scenario 'Debates are ordered randomly' do
scenario 'Debates are ordered randomly', :js do
create_list(:debate, 12)
visit debates_path
select 'random', from: 'order-selector'
expect(page).to have_select('order-selector', selected: 'random')
expect(page).to have_selector('#debates')
debates_first_time = find("#debates").text
select 'most commented', from: 'order-selector'
expect(page).to have_select('order-selector', selected: 'most commented')
expect(find("#debates .debate", match: :first)).to have_content(@most_commented_debate.title)
expect(page).to have_selector('#debates')
select 'random', from: 'order-selector'
expect(page).to have_select('order-selector', selected: 'random')
expect(page).to have_selector('#debates')
debates_second_time = find("#debates").text
expect(debates_first_time).to_not eq(debates_second_time)

View File

@@ -2,17 +2,14 @@ require 'rails_helper'
feature 'Votes' do
background do
@manuela = create(:user, verified_at: Time.now)
@pablo = create(:user)
login_as(@manuela)
end
feature 'Debates' do
background do
@manuela = create(:user, verified_at: Time.now)
@pablo = create(:user)
@debate = create(:debate)
login_as(@manuela)
visit debate_path(@debate)
end
scenario "Home shows user votes on featured debates" do
debate1 = create(:debate)
debate2 = create(:debate)
@@ -109,84 +106,86 @@ feature 'Votes' do
end
end
scenario 'Show no votes' do
visit debate_path(@debate)
expect(page).to have_content "No votes"
within('.in-favor') do
expect(page).to have_content "0%"
expect(page).to_not have_css("a.voted")
expect(page).to_not have_css("a.no-voted")
feature 'Single debate' do
background do
@debate = create(:debate)
end
within('.against') do
expect(page).to have_content "0%"
expect(page).to_not have_css("a.voted")
expect(page).to_not have_css("a.no-voted")
end
end
scenario 'Show no votes' do
visit debate_path(@debate)
scenario 'Show' do
create(:vote, voter: @manuela, votable: @debate, vote_flag: true)
create(:vote, voter: @pablo, votable: @debate, vote_flag: false)
visit debate_path(@debate)
expect(page).to have_content "2 votes"
within('.in-favor') do
expect(page).to have_content "50%"
expect(page).to have_css("a.voted")
end
within('.against') do
expect(page).to have_content "50%"
expect(page).to have_css("a.no-voted")
end
end
scenario 'Create from debate show', :js do
find('.in-favor a').click
within('.in-favor') do
expect(page).to have_content "100%"
expect(page).to have_css("a.voted")
end
within('.against') do
expect(page).to have_content "0%"
expect(page).to have_css("a.no-voted")
end
expect(page).to have_content "1 vote"
end
scenario 'Create from debate featured', :js do
visit root_path
within("#featured-debates") do
find('.in-favor a').click
expect(page).to have_content "No votes"
within('.in-favor') do
expect(page).to have_content "100%"
expect(page).to have_css("a.voted")
expect(page).to have_content "0%"
expect(page).to_not have_css("a.voted")
expect(page).to_not have_css("a.no-voted")
end
within('.against') do
expect(page).to have_content "0%"
expect(page).to_not have_css("a.voted")
expect(page).to_not have_css("a.no-voted")
end
end
scenario 'Update', :js do
visit debate_path(@debate)
find('.in-favor a').click
find('.against a').click
within('.in-favor') do
expect(page).to have_content "0%"
expect(page).to have_css("a.no-voted")
end
within('.against') do
expect(page).to have_content "100%"
expect(page).to have_css("a.voted")
end
expect(page).to have_content "1 vote"
end
expect(URI.parse(current_url).path).to eq(root_path)
end
scenario 'Create from debate index', :js do
visit debates_path
scenario 'Trying to vote multiple times', :js do
visit debate_path(@debate)
within("#debates") do
find('.in-favor a').click
find('.in-favor a').click
within('.in-favor') do
expect(page).to have_content "100%"
end
within('.against') do
expect(page).to have_content "0%"
end
expect(page).to have_content "1 vote"
end
scenario 'Show' do
create(:vote, voter: @manuela, votable: @debate, vote_flag: true)
create(:vote, voter: @pablo, votable: @debate, vote_flag: false)
visit debate_path(@debate)
expect(page).to have_content "2 votes"
within('.in-favor') do
expect(page).to have_content "50%"
expect(page).to have_css("a.voted")
end
within('.against') do
expect(page).to have_content "50%"
expect(page).to have_css("a.no-voted")
end
end
scenario 'Create from debate show', :js do
visit debate_path(@debate)
find('.in-favor a').click
@@ -202,51 +201,56 @@ feature 'Votes' do
expect(page).to have_content "1 vote"
end
expect(URI.parse(current_url).path).to eq(debates_path)
end
scenario 'Update', :js do
find('.in-favor a').click
find('.against a').click
scenario 'Create in featured', :js do
visit root_path
within('.in-favor') do
expect(page).to have_content "0%"
expect(page).to have_css("a.no-voted")
within("#featured-debates") do
find('.in-favor a').click
within('.in-favor') do
expect(page).to have_content "100%"
expect(page).to have_css("a.voted")
end
within('.against') do
expect(page).to have_content "0%"
expect(page).to have_css("a.no-voted")
end
expect(page).to have_content "1 vote"
end
expect(URI.parse(current_url).path).to eq(root_path)
end
within('.against') do
expect(page).to have_content "100%"
expect(page).to have_css("a.voted")
scenario 'Create in index', :js do
visit debates_path
within("#debates") do
find('.in-favor a').click
within('.in-favor') do
expect(page).to have_content "100%"
expect(page).to have_css("a.voted")
end
within('.against') do
expect(page).to have_content "0%"
expect(page).to have_css("a.no-voted")
end
expect(page).to have_content "1 vote"
end
expect(URI.parse(current_url).path).to eq(debates_path)
end
expect(page).to have_content "1 vote"
end
scenario 'Trying to vote multiple times', :js do
find('.in-favor a').click
find('.in-favor a').click
within('.in-favor') do
expect(page).to have_content "100%"
end
within('.against') do
expect(page).to have_content "0%"
end
expect(page).to have_content "1 vote"
end
end
feature 'Comments' do
background do
@manuela = create(:user)
@pablo = create(:user)
@debate = create(:debate)
@comment = create(:comment, commentable: @debate)
login_as(@manuela)
visit debate_path(@debate)
end
scenario 'Show' do
@@ -269,6 +273,8 @@ feature 'Votes' do
end
scenario 'Create', :js do
visit debate_path(@debate)
within("#comment_#{@comment.id}_votes") do
find(".in_favor a").click
@@ -285,6 +291,8 @@ feature 'Votes' do
end
scenario 'Update', :js do
visit debate_path(@debate)
within("#comment_#{@comment.id}_votes") do
find('.in_favor a').click
find('.against a').click
@@ -302,6 +310,8 @@ feature 'Votes' do
end
scenario 'Trying to vote multiple times', :js do
visit debate_path(@debate)
within("#comment_#{@comment.id}_votes") do
find('.in_favor a').click
find('.in_favor a').click
@@ -317,6 +327,5 @@ feature 'Votes' do
expect(page).to have_content "1 vote"
end
end
end
end

View File

@@ -174,15 +174,6 @@ describe Debate do
end
end
describe '#default_order' do
let!(:economy) { create(:debate) }
let!(:health) { create(:debate) }
it "returns debates ordered by last one first" do
expect(Debate.all).to eq([health, economy])
end
end
describe '#anonymous_votes_ratio' do
it "returns the percentage of anonymous votes of the total votes" do
debate = create(:debate, cached_anonymous_votes_total: 25, cached_votes_total: 100)
@@ -190,4 +181,64 @@ describe Debate do
end
end
describe '#hot_score' do
let(:now) { Time.now }
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
end
it "increases for debates with more comments" do
more_comments = create(:debate, :with_hot_score, created_at: now, comments_count: 10)
less_comments = create(:debate, :with_hot_score, created_at: now, comments_count: 1)
expect(more_comments.hot_score).to be > less_comments.hot_score
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
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
end
it "decays in older debates, even if they have more votes" do
older_more_voted = create(:debate, :with_hot_score, created_at: now - 2.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
end
describe 'actions which affect it' do
let(:debate) { create(:debate, :with_hot_score) }
it "increases with likes" do
previous = debate.hot_score
5.times { debate.register_vote(create(:user), true) }
expect(previous).to be < debate.hot_score
end
it "decreases with dislikes" do
debate.register_vote(create(:user), true)
previous = debate.hot_score
3.times { debate.register_vote(create(:user), false) }
expect(previous).to be > debate.hot_score
end
it "increases with comments" do
previous = debate.hot_score
Comment.create(user: create(:user), commentable: debate, body: 'foo')
expect(previous).to be < debate.hot_score
end
end
end
end

View File

@@ -1,6 +1,7 @@
RSpec::Matchers.define :appear_before do |later_content|
match do |earlier_content|
page.body.index(earlier_content) < page.body.index(later_content)
text = page.text
text.index(earlier_content) < text.index(later_content)
end
end