diff --git a/app/models/debate.rb b/app/models/debate.rb
index c2391a41f..d141e4330 100644
--- a/app/models/debate.rb
+++ b/app/models/debate.rb
@@ -102,28 +102,15 @@ class Debate < ActiveRecord::Base
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/20 # 1 positive vote / x comments
-
- 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**2 + weighted_score)*1000).round
+ self.hot_score = ScoreCalculator.hot_score(created_at,
+ cached_votes_total,
+ cached_votes_up,
+ comments_count)
end
def calculate_confidence_score
- return unless cached_votes_total > 0
- self.confidence_score = cached_votes_score * (cached_votes_up / cached_votes_total.to_f) * 100
+ self.confidence_score = ScoreCalculator.confidence_score(cached_votes_total,
+ cached_votes_up)
end
def self.search(terms)
diff --git a/app/models/proposal.rb b/app/models/proposal.rb
index 9e1b1daa6..24045e8f7 100644
--- a/app/models/proposal.rb
+++ b/app/models/proposal.rb
@@ -27,6 +27,8 @@ class Proposal < ActiveRecord::Base
before_validation :sanitize_tag_list
before_validation :set_responsible_name
+ before_save :calculate_hot_score, :calculate_confidence_score
+
scope :for_render, -> { includes(:tags) }
scope :sort_by_hot_score , -> { order(hot_score: :desc) }
scope :sort_by_confidence_score , -> { order(confidence_score: :desc) }
@@ -87,6 +89,22 @@ class Proposal < ActiveRecord::Base
"#{Setting.value_for("proposal_code_prefix")}-#{created_at.strftime('%Y-%M')}-#{id}"
end
+ def after_commented
+ save # updates the hot_score because there is a before_save
+ end
+
+ def calculate_hot_score
+ self.hot_score = ScoreCalculator.hot_score(created_at,
+ cached_votes_up,
+ cached_votes_up,
+ comments_count)
+ end
+
+ def calculate_confidence_score
+ self.confidence_score = ScoreCalculator.confidence_score(cached_votes_up,
+ cached_votes_up)
+ end
+
def self.title_max_length
@@title_max_length ||= self.columns.find { |c| c.name == 'title' }.limit || 80
end
@@ -126,6 +144,7 @@ class Proposal < ActiveRecord::Base
self.responsible_name = author.document_number
end
end
+
private
def validate_description_length
diff --git a/app/views/proposals/_form.html.erb b/app/views/proposals/_form.html.erb
index b2941bd44..1b2dcf77a 100644
--- a/app/views/proposals/_form.html.erb
+++ b/app/views/proposals/_form.html.erb
@@ -27,6 +27,13 @@
<%= f.cktext_area :description, maxlength: Proposal.description_max_length, ckeditor: { language: I18n.locale }, label: false %>
+
+
+
<%= f.label :external_url, t("proposals.form.proposal_external_url") %>
<%= f.text_field :external_url, placeholder: t("proposals.form.proposal_external_url"), label: false %>
diff --git a/app/views/welcome/highlights.html.erb b/app/views/welcome/highlights.html.erb
index 167f3a2e9..a0e95c7a2 100644
--- a/app/views/welcome/highlights.html.erb
+++ b/app/views/welcome/highlights.html.erb
@@ -16,6 +16,12 @@
+
+
+
<% end %>
\ No newline at end of file
diff --git a/config/locales/en.yml b/config/locales/en.yml
index 780b1fe30..fee411166 100644
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -178,6 +178,8 @@ en:
proposal_question_example_html: "Debe ser resumida en una pregunta cuya respuesta sea Sí o No. Ej. '¿Está usted de acuerdo en peatonalizar la calle Mayor?'"
proposal_text: Initial text for proposal
proposal_external_url: Link to additional documentation
+ proposal_external_video_url: "Enlace a vídeo externo"
+ proposal_external_video_url_note: "Puedes añadir un enlace a YouTube o Vimeo"
proposal_summary: "Proposal summary"
proposal_summary_note: "200 chars. maximum"
proposal_responsible_name: "First and last name of the person making this proposal"
diff --git a/config/locales/es.yml b/config/locales/es.yml
index 497be25de..bcffa07c7 100644
--- a/config/locales/es.yml
+++ b/config/locales/es.yml
@@ -180,6 +180,8 @@ es:
proposal_summary_note: "(máximo 200 caracteres)"
proposal_text: Texto desarrollado de la propuesta
proposal_external_url: Enlace a documentación adicional
+ proposal_external_video_url: "Enlace a vídeo externo"
+ proposal_external_video_url_note: "Puedes añadir un enlace a YouTube o Vimeo"
proposal_responsible_name: "Nombre y apellidos de la persona que hace esta propuesta"
proposal_responsible_name_note: "(individualmente o como representante de un colectivo; no se mostrará públicamente)"
tags_label: Temas
diff --git a/db/migrate/20150914181921_add_more_indexes_for_ahoy.rb b/db/migrate/20150914181921_add_more_indexes_for_ahoy.rb
new file mode 100644
index 000000000..7fa8bedc4
--- /dev/null
+++ b/db/migrate/20150914181921_add_more_indexes_for_ahoy.rb
@@ -0,0 +1,7 @@
+class AddMoreIndexesForAhoy < ActiveRecord::Migration
+ def change
+ add_index :ahoy_events, [:name, :time]
+ add_index :visits, [:started_at]
+ end
+end
+
diff --git a/db/migrate/20150914182652_adds_indexes.rb b/db/migrate/20150914182652_adds_indexes.rb
new file mode 100644
index 000000000..a22d0d16e
--- /dev/null
+++ b/db/migrate/20150914182652_adds_indexes.rb
@@ -0,0 +1,19 @@
+class AddsIndexes < ActiveRecord::Migration
+ def change
+ add_index :debates, :author_id
+ add_index :debates, [:author_id, :hidden_at]
+
+ add_index :proposals, :author_id
+ add_index :proposals, [:author_id, :hidden_at]
+ add_index :proposals, :cached_votes_up
+ add_index :proposals, :confidence_score
+ add_index :proposals, :hidden_at
+ add_index :proposals, :hot_score
+
+ add_index :settings, :key
+
+ add_index :verified_users, :document_number
+ add_index :verified_users, :phone
+ add_index :verified_users, :email
+ end
+end
diff --git a/db/migrate/20150914184018_add_indexes_for_searches.rb b/db/migrate/20150914184018_add_indexes_for_searches.rb
new file mode 100644
index 000000000..64c46370c
--- /dev/null
+++ b/db/migrate/20150914184018_add_indexes_for_searches.rb
@@ -0,0 +1,11 @@
+class AddIndexesForSearches < ActiveRecord::Migration
+ def change
+ add_index :debates, :title
+ add_index :debates, :description
+
+ add_index :proposals, :title
+ add_index :proposals, :question
+ add_index :proposals, :summary
+ add_index :proposals, :description
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 330444165..597933285 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -11,11 +11,10 @@
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema.define(version: 20150914173834) do
+ActiveRecord::Schema.define(version: 20150914184018) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
- enable_extension "unaccent"
create_table "addresses", force: :cascade do |t|
t.integer "user_id"
@@ -52,6 +51,7 @@ ActiveRecord::Schema.define(version: 20150914173834) do
t.string "ip"
end
+ add_index "ahoy_events", ["name", "time"], name: "index_ahoy_events_on_name_and_time", using: :btree
add_index "ahoy_events", ["time"], name: "index_ahoy_events_on_time", using: :btree
add_index "ahoy_events", ["user_id"], name: "index_ahoy_events_on_user_id", using: :btree
add_index "ahoy_events", ["visit_id"], name: "index_ahoy_events_on_visit_id", using: :btree
@@ -105,13 +105,17 @@ ActiveRecord::Schema.define(version: 20150914173834) do
t.integer "confidence_score", default: 0
end
+ add_index "debates", ["author_id", "hidden_at"], name: "index_debates_on_author_id_and_hidden_at", using: :btree
+ add_index "debates", ["author_id"], name: "index_debates_on_author_id", using: :btree
add_index "debates", ["cached_votes_down"], name: "index_debates_on_cached_votes_down", using: :btree
add_index "debates", ["cached_votes_score"], name: "index_debates_on_cached_votes_score", using: :btree
add_index "debates", ["cached_votes_total"], name: "index_debates_on_cached_votes_total", using: :btree
add_index "debates", ["cached_votes_up"], name: "index_debates_on_cached_votes_up", using: :btree
add_index "debates", ["confidence_score"], name: "index_debates_on_confidence_score", using: :btree
+ add_index "debates", ["description"], name: "index_debates_on_description", using: :btree
add_index "debates", ["hidden_at"], name: "index_debates_on_hidden_at", using: :btree
add_index "debates", ["hot_score"], name: "index_debates_on_hot_score", using: :btree
+ add_index "debates", ["title"], name: "index_debates_on_title", using: :btree
create_table "delayed_jobs", force: :cascade do |t|
t.integer "priority", default: 0, null: false
@@ -166,7 +170,7 @@ ActiveRecord::Schema.define(version: 20150914173834) do
create_table "locks", force: :cascade do |t|
t.integer "user_id"
t.integer "tries", default: 0
- t.datetime "locked_until", default: '2015-09-10 13:46:11', null: false
+ t.datetime "locked_until", default: '2015-09-11 17:24:30', null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end
@@ -209,11 +213,24 @@ ActiveRecord::Schema.define(version: 20150914173834) do
t.text "summary"
end
+ add_index "proposals", ["author_id", "hidden_at"], name: "index_proposals_on_author_id_and_hidden_at", using: :btree
+ add_index "proposals", ["author_id"], name: "index_proposals_on_author_id", using: :btree
+ add_index "proposals", ["cached_votes_up"], name: "index_proposals_on_cached_votes_up", using: :btree
+ add_index "proposals", ["confidence_score"], name: "index_proposals_on_confidence_score", using: :btree
+ add_index "proposals", ["description"], name: "index_proposals_on_description", using: :btree
+ add_index "proposals", ["hidden_at"], name: "index_proposals_on_hidden_at", using: :btree
+ add_index "proposals", ["hot_score"], name: "index_proposals_on_hot_score", using: :btree
+ add_index "proposals", ["question"], name: "index_proposals_on_question", using: :btree
+ add_index "proposals", ["summary"], name: "index_proposals_on_summary", using: :btree
+ add_index "proposals", ["title"], name: "index_proposals_on_title", using: :btree
+
create_table "settings", force: :cascade do |t|
t.string "key"
t.string "value"
end
+ add_index "settings", ["key"], name: "index_settings_on_key", using: :btree
+
create_table "simple_captcha_data", force: :cascade do |t|
t.string "key", limit: 40
t.string "value", limit: 6
@@ -296,6 +313,10 @@ ActiveRecord::Schema.define(version: 20150914173834) do
t.datetime "updated_at", null: false
end
+ add_index "verified_users", ["document_number"], name: "index_verified_users_on_document_number", using: :btree
+ add_index "verified_users", ["email"], name: "index_verified_users_on_email", using: :btree
+ add_index "verified_users", ["phone"], name: "index_verified_users_on_phone", using: :btree
+
create_table "visits", id: :uuid, default: nil, force: :cascade do |t|
t.uuid "visitor_id"
t.string "ip"
@@ -324,6 +345,7 @@ ActiveRecord::Schema.define(version: 20150914173834) do
t.datetime "started_at"
end
+ add_index "visits", ["started_at"], name: "index_visits_on_started_at", using: :btree
add_index "visits", ["user_id"], name: "index_visits_on_user_id", using: :btree
create_table "votes", force: :cascade do |t|
diff --git a/lib/score_calculator.rb b/lib/score_calculator.rb
new file mode 100644
index 000000000..7d77e82d6
--- /dev/null
+++ b/lib/score_calculator.rb
@@ -0,0 +1,30 @@
+module ScoreCalculator
+
+ EPOC = Time.new(2015, 6, 15)
+ COMMENT_WEIGHT = 1.0/20 # 1 positive vote / x comments
+ TIME_UNIT = 12.hours.to_f
+
+ 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.now) - EPOC).to_f
+
+ (((offset * sign) + (seconds/TIME_UNIT)) * 10000000).round
+ end
+
+ def self.confidence_score(votes_total, votes_up)
+ return 0 unless votes_total > 0
+
+ votes_total = votes_total.to_f
+ votes_up = votes_up.to_f
+ votes_down = votes_total - votes_up
+ score = votes_up - votes_down
+
+ score * (votes_up / votes_total) * 100
+ end
+
+end
diff --git a/spec/features/highlights_spec.rb b/spec/features/highlights_spec.rb
index 20ca7a0ec..33ade0063 100644
--- a/spec/features/highlights_spec.rb
+++ b/spec/features/highlights_spec.rb
@@ -22,4 +22,13 @@ feature "Highlights" do
expect('worst proposal 60').to appear_before('worst debate 50')
end
+ scenario 'create debate and create proposal links' do
+ login_as(create(:user))
+
+ visit highlights_path
+
+ expect(page).to have_link("Start a proposal")
+ expect(page).to have_link("Start a debate")
+ end
+
end
\ No newline at end of file
diff --git a/spec/models/debate_spec.rb b/spec/models/debate_spec.rb
index 55358cddc..907bcef5b 100644
--- a/spec/models/debate_spec.rb
+++ b/spec/models/debate_spec.rb
@@ -191,7 +191,7 @@ describe Debate do
end
it "increases for debates with more comments" do
- more_comments = create(:debate, :with_hot_score, created_at: now, comments_count: 10)
+ 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
end
@@ -232,8 +232,8 @@ describe Debate do
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
+ 25.times{ Comment.create(user: create(:user), commentable: debate, body: 'foobarbaz') }
+ expect(previous).to be < debate.reload.hot_score
end
end
end
@@ -244,16 +244,16 @@ describe Debate do
debate = create(:debate, :with_confidence_score, cached_votes_up: 100, cached_votes_score: 100, cached_votes_total: 100)
expect(debate.confidence_score).to eq(10000)
- debate = create(:debate, :with_confidence_score, cached_votes_up: 0, cached_votes_score: -100, cached_votes_total: 100)
+ debate = create(:debate, :with_confidence_score, cached_votes_up: 0, cached_votes_total: 100)
expect(debate.confidence_score).to eq(0)
- debate = create(:debate, :with_confidence_score, cached_votes_up: 50, cached_votes_score: 50, cached_votes_total: 100)
- expect(debate.confidence_score).to eq(2500)
+ debate = create(:debate, :with_confidence_score, cached_votes_up: 75, cached_votes_total: 100)
+ expect(debate.confidence_score).to eq(3750)
- debate = create(:debate, :with_confidence_score, cached_votes_up: 500, cached_votes_score: 500, cached_votes_total: 1000)
- expect(debate.confidence_score).to eq(25000)
+ debate = create(:debate, :with_confidence_score, cached_votes_up: 750, cached_votes_total: 1000)
+ expect(debate.confidence_score).to eq(37500)
- debate = create(:debate, :with_confidence_score, cached_votes_up: 10, cached_votes_score: -80, cached_votes_total: 100)
+ debate = create(:debate, :with_confidence_score, cached_votes_up: 10, cached_votes_total: 100)
expect(debate.confidence_score).to eq(-800)
end
@@ -396,4 +396,4 @@ describe Debate do
end
-end
\ No newline at end of file
+end
diff --git a/spec/models/proposal_spec.rb b/spec/models/proposal_spec.rb
index fe2422173..f713f3ce5 100644
--- a/spec/models/proposal_spec.rb
+++ b/spec/models/proposal_spec.rb
@@ -142,4 +142,85 @@ describe Proposal do
end
end
end
-end
\ No newline at end of file
+
+ describe '#hot_score' do
+ let(:now) { Time.now }
+
+ 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
+ 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
+ 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
+ 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
+ end
+
+ it "decays in older proposals, even if they have more votes" do
+ older_more_voted = create(:proposal, :with_hot_score, created_at: now - 2.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
+ 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.now), true) }
+ expect(previous).to be < proposal.reload.hot_score
+ end
+
+ it "increases with comments" do
+ previous = proposal.hot_score
+ 25.times{ Comment.create(user: create(:user), commentable: proposal, body: 'foobarbaz') }
+ expect(previous).to be < proposal.reload.hot_score
+ end
+ end
+ end
+
+ describe "#confidence_score" do
+
+ it "takes into account votes" do
+ proposal = create(:proposal, :with_confidence_score, cached_votes_up: 100)
+ expect(proposal.confidence_score).to eq(10000)
+
+ proposal = create(:proposal, :with_confidence_score, cached_votes_up: 0)
+ expect(proposal.confidence_score).to eq(0)
+
+ proposal = create(:proposal, :with_confidence_score, cached_votes_up: 75)
+ expect(proposal.confidence_score).to eq(7500)
+
+ proposal = create(:proposal, :with_confidence_score, cached_votes_up: 750)
+ expect(proposal.confidence_score).to eq(75000)
+
+ proposal = create(:proposal, :with_confidence_score, cached_votes_up: 10)
+ expect(proposal.confidence_score).to eq(1000)
+ end
+
+ describe 'actions which affect it' do
+ let(:proposal) { create(:proposal, :with_confidence_score) }
+
+ it "increases with like" do
+ previous = proposal.confidence_score
+ 5.times { proposal.register_vote(create(:user, verified_at: Time.now), true) }
+ expect(previous).to be < proposal.confidence_score
+ end
+ end
+
+ end
+end