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