From cf0b7a53b0001581827c98b4c76e34471d804932 Mon Sep 17 00:00:00 2001 From: kikito Date: Fri, 20 May 2016 17:55:51 +0200 Subject: [PATCH 01/13] Adds missing budget_id to investments --- db/migrate/20160520151954_add_budget_id_to_investments.rb | 5 +++++ db/schema.rb | 2 ++ 2 files changed, 7 insertions(+) create mode 100644 db/migrate/20160520151954_add_budget_id_to_investments.rb diff --git a/db/migrate/20160520151954_add_budget_id_to_investments.rb b/db/migrate/20160520151954_add_budget_id_to_investments.rb new file mode 100644 index 000000000..8860be67c --- /dev/null +++ b/db/migrate/20160520151954_add_budget_id_to_investments.rb @@ -0,0 +1,5 @@ +class AddBudgetIdToInvestments < ActiveRecord::Migration + def change + add_reference :budget_investments, :budget, index: true + end +end diff --git a/db/schema.rb b/db/schema.rb index c6f6ffe67..cf0f2dff9 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -111,11 +111,13 @@ ActiveRecord::Schema.define(version: 20160523143320) do t.datetime "created_at", null: false t.datetime "updated_at", null: false t.integer "heading_id" + t.integer "budget_id" end add_index "budget_investments", ["administrator_id"], name: "index_budget_investments_on_administrator_id", using: :btree add_index "budget_investments", ["author_id"], name: "index_budget_investments_on_author_id", using: :btree add_index "budget_investments", ["heading_id"], name: "index_budget_investments_on_heading_id", using: :btree + add_index "budget_investments", ["budget_id"], name: "index_budget_investments_on_budget_id", using: :btree add_index "budget_investments", ["tsv"], name: "index_budget_investments_on_tsv", using: :gin create_table "budgets", force: :cascade do |t| From fdc8636e12daf2ae70b32216a9145b09c5a6d26f Mon Sep 17 00:00:00 2001 From: kikito Date: Fri, 20 May 2016 17:56:05 +0200 Subject: [PATCH 02/13] Adds simple Budget specs --- app/models/budget.rb | 10 +++++++++- spec/factories.rb | 5 +++++ spec/models/budget_spec.rb | 15 +++++++++++++++ 3 files changed, 29 insertions(+), 1 deletion(-) create mode 100644 spec/models/budget_spec.rb diff --git a/app/models/budget.rb b/app/models/budget.rb index abffbf799..9b5940e4f 100644 --- a/app/models/budget.rb +++ b/app/models/budget.rb @@ -1,5 +1,12 @@ class Budget < ActiveRecord::Base + VALID_PHASES = %W{on_hold accepting selecting balloting finished} + + validates :phase, inclusion: { in: VALID_PHASES } + + has_many :investments + has_many :ballots + def on_hold? phase == "on_hold" end @@ -20,4 +27,5 @@ class Budget < ActiveRecord::Base phase == "finished" end -end \ No newline at end of file +end + diff --git a/spec/factories.rb b/spec/factories.rb index 4aa3f6718..0ae660e9a 100644 --- a/spec/factories.rb +++ b/spec/factories.rb @@ -358,4 +358,9 @@ FactoryGirl.define do sequence(:name) { |n| "District #{n}" } census_code { '01' } end + + factory :budget do + sequence(:name) {|n| "Budget #{n}" } + phase "on_hold" + end end diff --git a/spec/models/budget_spec.rb b/spec/models/budget_spec.rb new file mode 100644 index 000000000..d874148a1 --- /dev/null +++ b/spec/models/budget_spec.rb @@ -0,0 +1,15 @@ +require 'rails_helper' + +describe Budget do + it "validates the phase" do + budget = create(:budget) + Budget::VALID_PHASES.each do |phase| + budget.phase = phase + expect(budget).to be_valid + end + + budget.phase = 'inexisting' + expect(budget).to_not be_valid + end +end + From 20108639cbb912e0da8c6e68ca5cfc9292d281a6 Mon Sep 17 00:00:00 2001 From: kikito Date: Mon, 23 May 2016 17:35:49 +0200 Subject: [PATCH 03/13] migrates --- ...5730_create_budget_valuator_assignments.rb | 9 ++++++ ...d_budget_investments_count_to_valuators.rb | 5 ++++ ...0160523150146_rename_bi_valuation_count.rb | 5 ++++ db/schema.rb | 30 ++++++++++++------- 4 files changed, 39 insertions(+), 10 deletions(-) create mode 100644 db/migrate/20160523095730_create_budget_valuator_assignments.rb create mode 100644 db/migrate/20160523144313_add_budget_investments_count_to_valuators.rb create mode 100644 db/migrate/20160523150146_rename_bi_valuation_count.rb diff --git a/db/migrate/20160523095730_create_budget_valuator_assignments.rb b/db/migrate/20160523095730_create_budget_valuator_assignments.rb new file mode 100644 index 000000000..18686da0e --- /dev/null +++ b/db/migrate/20160523095730_create_budget_valuator_assignments.rb @@ -0,0 +1,9 @@ +class CreateBudgetValuatorAssignments < ActiveRecord::Migration + def change + create_table :budget_valuator_assignments, index: false do |t| + t.belongs_to :valuator + t.integer :investment_id, index: true + t.timestamps null: false + end + end +end diff --git a/db/migrate/20160523144313_add_budget_investments_count_to_valuators.rb b/db/migrate/20160523144313_add_budget_investments_count_to_valuators.rb new file mode 100644 index 000000000..9ec741951 --- /dev/null +++ b/db/migrate/20160523144313_add_budget_investments_count_to_valuators.rb @@ -0,0 +1,5 @@ +class AddBudgetInvestmentsCountToValuators < ActiveRecord::Migration + def change + add_column :valuators, :budget_investments_count, :integer, default: 0 + end +end diff --git a/db/migrate/20160523150146_rename_bi_valuation_count.rb b/db/migrate/20160523150146_rename_bi_valuation_count.rb new file mode 100644 index 000000000..b7b41e6e1 --- /dev/null +++ b/db/migrate/20160523150146_rename_bi_valuation_count.rb @@ -0,0 +1,5 @@ +class RenameBiValuationCount < ActiveRecord::Migration + def change + rename_column :budget_investments, :valuation_assignments_count, :valuator_assignments_count + end +end diff --git a/db/schema.rb b/db/schema.rb index cf0f2dff9..af15b752c 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20160523143320) do +ActiveRecord::Schema.define(version: 20160523150146) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -93,20 +93,20 @@ ActiveRecord::Schema.define(version: 20160523143320) do t.string "title" t.text "description" t.string "external_url" - t.integer "price", limit: 8 - t.string "feasibility", limit: 15, default: "undecided" + t.integer "price", limit: 8 + t.string "feasibility", limit: 15, default: "undecided" t.text "price_explanation" t.text "unfeasibility_explanation" t.text "internal_comments" - t.boolean "valuation_finished", default: false - t.integer "valuation_assignments_count", default: 0 - t.integer "price_first_year", limit: 8 + t.boolean "valuation_finished", default: false + t.integer "valuator_assignments_count", default: 0 + t.integer "price_first_year", limit: 8 t.string "duration" t.datetime "hidden_at" - t.integer "cached_votes_up", default: 0 - t.integer "comments_count", default: 0 - t.integer "confidence_score", default: 0, null: false - t.integer "physical_votes", default: 0 + t.integer "cached_votes_up", default: 0 + t.integer "comments_count", default: 0 + t.integer "confidence_score", default: 0, null: false + t.integer "physical_votes", default: 0 t.tsvector "tsv" t.datetime "created_at", null: false t.datetime "updated_at", null: false @@ -120,6 +120,15 @@ ActiveRecord::Schema.define(version: 20160523143320) do add_index "budget_investments", ["budget_id"], name: "index_budget_investments_on_budget_id", using: :btree add_index "budget_investments", ["tsv"], name: "index_budget_investments_on_tsv", using: :gin + create_table "budget_valuator_assignments", force: :cascade do |t| + t.integer "valuator_id" + t.integer "investment_id" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + end + + add_index "budget_valuator_assignments", ["investment_id"], name: "index_budget_valuator_assignments_on_investment_id", using: :btree + create_table "budgets", force: :cascade do |t| t.string "name", limit: 30 t.text "description" @@ -513,6 +522,7 @@ ActiveRecord::Schema.define(version: 20160523143320) do t.integer "user_id" t.string "description" t.integer "spending_proposals_count", default: 0 + t.integer "budget_investments_count", default: 0 end add_index "valuators", ["user_id"], name: "index_valuators_on_user_id", using: :btree From 60cf683a0b7665aa7ba78353875b4d042b6d7c77 Mon Sep 17 00:00:00 2001 From: kikito Date: Mon, 23 May 2016 17:42:59 +0200 Subject: [PATCH 04/13] Adds valuator_assignments --- app/models/budget/investment.rb | 10 +++++----- app/models/budget/valuator_assignment.rb | 6 ++++++ 2 files changed, 11 insertions(+), 5 deletions(-) create mode 100644 app/models/budget/valuator_assignment.rb diff --git a/app/models/budget/investment.rb b/app/models/budget/investment.rb index a736751ad..d222dfcd2 100644 --- a/app/models/budget/investment.rb +++ b/app/models/budget/investment.rb @@ -14,8 +14,8 @@ class Budget belongs_to :heading belongs_to :administrator - has_many :valuation_assignments, dependent: :destroy - has_many :valuators, through: :valuation_assignments + has_many :valuator_assignments, dependent: :destroy + has_many :valuators, through: :valuator_assignments has_many :comments, as: :commentable validates :title, presence: true @@ -33,8 +33,8 @@ class Budget scope :valuation_open, -> { where(valuation_finished: false) } scope :without_admin, -> { valuation_open.where(administrator_id: nil) } - scope :managed, -> { valuation_open.where(valuation_assignments_count: 0).where("administrator_id IS NOT ?", nil) } - scope :valuating, -> { valuation_open.where("valuation_assignments_count > 0 AND valuation_finished = ?", false) } + scope :managed, -> { valuation_open.where(valuator_assignments_count: 0).where("administrator_id IS NOT ?", nil) } + scope :valuating, -> { valuation_open.where("valuator_assignments_count > 0 AND valuation_finished = ?", false) } scope :valuation_finished, -> { where(valuation_finished: true) } scope :feasible, -> { where(feasible: true) } scope :unfeasible, -> { valuation_finished.where(feasible: false) } @@ -44,7 +44,7 @@ class Budget scope :by_budget, -> (budget_id) { where(budget_id: budget_id) } scope :by_admin, -> (admin_id) { where(administrator_id: admin_id) } scope :by_tag, -> (tag_name) { tagged_with(tag_name) } - scope :by_valuator, -> (valuator_id) { where("valuation_assignments.valuator_id = ?", valuator_id).joins(:valuation_assignments) } + scope :by_valuator, -> (valuator_id) { where("budget_valuator_assignments.valuator_id = ?", valuator_id).joins(:valuator_assignments) } scope :for_render, -> { includes(heading: :geozone) } diff --git a/app/models/budget/valuator_assignment.rb b/app/models/budget/valuator_assignment.rb new file mode 100644 index 000000000..18ef73812 --- /dev/null +++ b/app/models/budget/valuator_assignment.rb @@ -0,0 +1,6 @@ +class Budget + class ValuatorAssignment < ActiveRecord::Base + belongs_to :valuator, counter_cache: :budget_investments_count + belongs_to :investment, counter_cache: true + end +end From d4d16d3006325687c858aacd347a635b78529036 Mon Sep 17 00:00:00 2001 From: kikito Date: Mon, 23 May 2016 19:49:49 +0200 Subject: [PATCH 05/13] Adds responsible_name migration for investments --- ...3164449_add_responsible_name_to_budget_investments.rb | 5 +++++ db/schema.rb | 9 +++++---- 2 files changed, 10 insertions(+), 4 deletions(-) create mode 100644 db/migrate/20160523164449_add_responsible_name_to_budget_investments.rb diff --git a/db/migrate/20160523164449_add_responsible_name_to_budget_investments.rb b/db/migrate/20160523164449_add_responsible_name_to_budget_investments.rb new file mode 100644 index 000000000..46576d059 --- /dev/null +++ b/db/migrate/20160523164449_add_responsible_name_to_budget_investments.rb @@ -0,0 +1,5 @@ +class AddResponsibleNameToBudgetInvestments < ActiveRecord::Migration + def change + add_column :budget_investments, :responsible_name, :string + end +end diff --git a/db/schema.rb b/db/schema.rb index af15b752c..ed8e3ed58 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20160523150146) do +ActiveRecord::Schema.define(version: 20160523164449) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -108,16 +108,17 @@ ActiveRecord::Schema.define(version: 20160523150146) do t.integer "confidence_score", default: 0, null: false t.integer "physical_votes", default: 0 t.tsvector "tsv" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false t.integer "heading_id" t.integer "budget_id" + t.string "responsible_name" end add_index "budget_investments", ["administrator_id"], name: "index_budget_investments_on_administrator_id", using: :btree add_index "budget_investments", ["author_id"], name: "index_budget_investments_on_author_id", using: :btree - add_index "budget_investments", ["heading_id"], name: "index_budget_investments_on_heading_id", using: :btree add_index "budget_investments", ["budget_id"], name: "index_budget_investments_on_budget_id", using: :btree + add_index "budget_investments", ["heading_id"], name: "index_budget_investments_on_heading_id", using: :btree add_index "budget_investments", ["tsv"], name: "index_budget_investments_on_tsv", using: :gin create_table "budget_valuator_assignments", force: :cascade do |t| From d663e4c59476d315cc976d7d7922b4392a29ec3d Mon Sep 17 00:00:00 2001 From: kikito Date: Mon, 23 May 2016 19:50:11 +0200 Subject: [PATCH 06/13] Adds/improves factories --- spec/factories.rb | 32 +++++++++++++++++++++++--------- 1 file changed, 23 insertions(+), 9 deletions(-) diff --git a/spec/factories.rb b/spec/factories.rb index 0ae660e9a..85047b511 100644 --- a/spec/factories.rb +++ b/spec/factories.rb @@ -1,5 +1,4 @@ FactoryGirl.define do - sequence(:document_number) { |n| "#{n.to_s.rjust(8, '0')}X" } factory :user do @@ -192,6 +191,19 @@ FactoryGirl.define do factory :budget do sequence(:name) { |n| "Budget #{n}" } currency_symbol "€" + phase 'on_hold' + + trait :selecting do + phase 'selecting' + end + + trait :balloting do + phase 'balloting' + end + + trait :finished do + phase 'finished' + end end factory :budget_heading, class: 'Budget::Heading' do @@ -200,13 +212,18 @@ FactoryGirl.define do price 1000000 end - factory :budget_investment, class: 'Budget::Investment' do - sequence(:title) { |n| "Investment #{n} title" } + factory :budget_investment, class: Budget::Investment do + sequence(:title) { |n| "Budget Investment #{n} title" } + association :budget + association :author, factory: :user description 'Spend money on this' - price 1000 + unfeasibility_explanation '' external_url 'http://external_documention.org' terms_of_service '1' - association :author, factory: :user + + trait :with_confidence_score do + before(:save) { |i| i.calculate_confidence_score } + end trait :feasible do feasibility "feasible" @@ -359,8 +376,5 @@ FactoryGirl.define do census_code { '01' } end - factory :budget do - sequence(:name) {|n| "Budget #{n}" } - phase "on_hold" - end + end From 673cfc82493f7ed7f33638048f6d5e8a9cebfc04 Mon Sep 17 00:00:00 2001 From: kikito Date: Mon, 23 May 2016 19:51:02 +0200 Subject: [PATCH 07/13] Implements some basic budget investment specs --- app/models/budget.rb | 5 + app/models/budget/investment.rb | 20 +- spec/models/budget/investment_spec.rb | 382 ++++++++++++++++++++++++++ 3 files changed, 397 insertions(+), 10 deletions(-) create mode 100644 spec/models/budget/investment_spec.rb diff --git a/app/models/budget.rb b/app/models/budget.rb index 9b5940e4f..dc971bc36 100644 --- a/app/models/budget.rb +++ b/app/models/budget.rb @@ -6,6 +6,7 @@ class Budget < ActiveRecord::Base has_many :investments has_many :ballots + has_many :headings def on_hold? phase == "on_hold" @@ -27,5 +28,9 @@ class Budget < ActiveRecord::Base phase == "finished" end + def amount_available(heading) + return 0 unless heading_ids.include?(heading.try(:id)) + heading.try(:price) || 10000 # FIXME + end end diff --git a/app/models/budget/investment.rb b/app/models/budget/investment.rb index d222dfcd2..bc7c81072 100644 --- a/app/models/budget/investment.rb +++ b/app/models/budget/investment.rb @@ -10,6 +10,7 @@ class Budget acts_as_paranoid column: :hidden_at include ActsAsParanoidAliases + belongs_to :budget belongs_to :author, -> { with_hidden }, class_name: 'User', foreign_key: 'author_id' belongs_to :heading belongs_to :administrator @@ -23,8 +24,8 @@ class Budget validates :description, presence: true validates_presence_of :unfeasibility_explanation, if: :unfeasibility_explanation_required? - validates :title, length: { in: 4..Investment.title_max_length } - validates :description, length: { maximum: Investment.description_max_length } + validates :title, length: { in: 4 .. Budget::Investment.title_max_length } + validates :description, length: { maximum: Budget::Investment.description_max_length } validates :terms_of_service, acceptance: { allow_nil: false }, on: :create scope :sort_by_confidence_score, -> { reorder(confidence_score: :desc, id: :desc) } @@ -36,9 +37,9 @@ class Budget scope :managed, -> { valuation_open.where(valuator_assignments_count: 0).where("administrator_id IS NOT ?", nil) } scope :valuating, -> { valuation_open.where("valuator_assignments_count > 0 AND valuation_finished = ?", false) } scope :valuation_finished, -> { where(valuation_finished: true) } - scope :feasible, -> { where(feasible: true) } - scope :unfeasible, -> { valuation_finished.where(feasible: false) } - scope :not_unfeasible, -> { where("feasible IS ? OR feasible = ?", nil, true) } + scope :feasible, -> { where(feasibility: "feasible") } + scope :unfeasible, -> { where(feasibility: "unfeasible") } + scope :undecided, -> { where(feasibility: "undecided") } scope :with_supports, -> { where('cached_votes_up > 0') } scope :by_budget, -> (budget_id) { where(budget_id: budget_id) } @@ -143,14 +144,14 @@ class Budget def reason_for_not_being_selectable_by(user) return permission_problem(user) if permission_problem?(user) - return :not_voting_allowed unless budget.selecting? + return :no_selecting_allowed unless budget.selecting? end def reason_for_not_being_ballotable_by(user, ballot) return permission_problem(user) if permission_problem?(user) return :no_ballots_allowed unless budget.balloting? return :different_heading_assigned unless heading_id.blank? || ballot.blank? || heading_id == ballot.heading_id || ballot.heading_id.nil? - return :not_enough_money unless enough_money?(ballot) + return :not_enough_money if ballot.present? && !enough_money? end def permission_problem(user) @@ -172,9 +173,8 @@ class Budget reason_for_not_being_ballotable_by(user).blank? end - def enough_money?(ballot) - return true if ballot.blank? - available_money = ballot.amount_available(heading) + def enough_money? + available_money = budget.amount_available(self.heading) price.to_i <= available_money end diff --git a/spec/models/budget/investment_spec.rb b/spec/models/budget/investment_spec.rb new file mode 100644 index 000000000..d8563574e --- /dev/null +++ b/spec/models/budget/investment_spec.rb @@ -0,0 +1,382 @@ +require 'rails_helper' + +describe Budget::Investment do + let(:investment) { build(:budget_investment) } + + it "should be valid" do + expect(investment).to be_valid + end + + it "should not be valid without an author" do + investment.author = nil + expect(investment).to_not be_valid + end + + describe "#title" do + it "should not be valid without a title" do + investment.title = nil + expect(investment).to_not be_valid + end + + it "should not be valid when very short" do + investment.title = "abc" + expect(investment).to_not be_valid + end + + it "should not be valid when very long" do + investment.title = "a" * 81 + expect(investment).to_not be_valid + end + end + + it "sanitizes description" do + investment.description = "" + investment.valid? + expect(investment.description).to eq("alert('danger');") + end + + describe "#unfeasibility_explanation" do + it "should be valid if valuation not finished" do + investment.unfeasibility_explanation = "" + investment.valuation_finished = false + expect(investment).to be_valid + end + + it "should be valid if valuation finished and feasible" do + investment.unfeasibility_explanation = "" + investment.feasibility = "feasible" + investment.valuation_finished = true + expect(investment).to be_valid + end + + it "should not be valid if valuation finished and unfeasible" do + investment.unfeasibility_explanation = "" + investment.feasibility = "unfeasible" + investment.valuation_finished = true + expect(investment).to_not be_valid + end + end + + describe "#code" do + it "returns the investment and budget id" do + investment = create(:budget_investment) + expect(investment.code).to include("#{investment.id}") + expect(investment.code).to include("#{investment.budget.id}") + end + end + + describe "by_admin" do + it "should return spending investments assigned to specific administrator" do + investment1 = create(:budget_investment, administrator_id: 33) + create(:budget_investment) + + by_admin = Budget::Investment.by_admin(33) + + expect(by_admin.size).to eq(1) + expect(by_admin.first).to eq(investment1) + end + end + + describe "by_valuator" do + it "should return spending proposals assigned to specific valuator" do + investment1 = create(:budget_investment) + investment2 = create(:budget_investment) + investment3 = create(:budget_investment) + + valuator1 = create(:valuator) + valuator2 = create(:valuator) + + investment1.valuators << valuator1 + investment2.valuators << valuator2 + investment3.valuators << [valuator1, valuator2] + + by_valuator = Budget::Investment.by_valuator(valuator1.id) + + expect(by_valuator.size).to eq(2) + expect(by_valuator.sort).to eq([investment1,investment3].sort) + end + end + + describe "scopes" do + describe "valuation_open" do + it "should return all spending proposals with false valuation_finished" do + investment1 = create(:budget_investment, valuation_finished: true) + investment2 = create(:budget_investment) + + valuation_open = Budget::Investment.valuation_open + + expect(valuation_open.size).to eq(1) + expect(valuation_open.first).to eq(investment2) + end + end + + describe "without_admin" do + it "should return all open spending proposals without assigned admin" do + investment1 = create(:budget_investment, valuation_finished: true) + investment2 = create(:budget_investment, administrator: create(:administrator)) + investment3 = create(:budget_investment) + + without_admin = Budget::Investment.without_admin + + expect(without_admin.size).to eq(1) + expect(without_admin.first).to eq(investment3) + end + end + + describe "managed" do + it "should return all open spending proposals with assigned admin but without assigned valuators" do + investment1 = create(:budget_investment, administrator: create(:administrator)) + investment2 = create(:budget_investment, administrator: create(:administrator), valuation_finished: true) + investment3 = create(:budget_investment, administrator: create(:administrator)) + investment1.valuators << create(:valuator) + + managed = Budget::Investment.managed + + expect(managed.size).to eq(1) + expect(managed.first).to eq(investment3) + end + end + + describe "valuating" do + it "should return all spending proposals with assigned valuator but valuation not finished" do + investment1 = create(:budget_investment) + investment2 = create(:budget_investment) + investment3 = create(:budget_investment, valuation_finished: true) + + investment2.valuators << create(:valuator) + investment3.valuators << create(:valuator) + + valuating = Budget::Investment.valuating + + expect(valuating.size).to eq(1) + expect(valuating.first).to eq(investment2) + end + end + + describe "valuation_finished" do + it "should return all spending proposals with valuation finished" do + investment1 = create(:budget_investment) + investment2 = create(:budget_investment) + investment3 = create(:budget_investment, valuation_finished: true) + + investment2.valuators << create(:valuator) + investment3.valuators << create(:valuator) + + valuation_finished = Budget::Investment.valuation_finished + + expect(valuation_finished.size).to eq(1) + expect(valuation_finished.first).to eq(investment3) + end + end + + describe "feasible" do + it "should return all feasible spending proposals" do + feasible_investment = create(:budget_investment, :feasible) + create(:budget_investment) + + expect(Budget::Investment.feasible).to eq [feasible_investment] + end + end + + describe "unfeasible" do + it "should return all unfeasible spending proposals" do + unfeasible_investment = create(:budget_investment, :unfeasible) + create(:budget_investment, :feasible) + + expect(Budget::Investment.unfeasible).to eq [unfeasible_investment] + end + end + end + + describe 'Permissions' do + let(:budget) { create(:budget) } + let(:heading) { create(:budget_heading, budget: budget) } + let(:user) { create(:user, :level_two) } + let(:luser) { create(:user) } + let(:city_sp) { create(:budget_investment, budget: budget) } + let(:district_sp) { create(:budget_investment, budget: budget, heading: heading) } + + describe '#reason_for_not_being_selectable_by' do + it "rejects not logged in users" do + expect(city_sp.reason_for_not_being_selectable_by(nil)).to eq(:not_logged_in) + expect(district_sp.reason_for_not_being_selectable_by(nil)).to eq(:not_logged_in) + end + + it "rejects not verified users" do + expect(city_sp.reason_for_not_being_selectable_by(luser)).to eq(:not_verified) + expect(district_sp.reason_for_not_being_selectable_by(luser)).to eq(:not_verified) + end + + it "rejects organizations" do + create(:organization, user: user) + expect(city_sp.reason_for_not_being_selectable_by(user)).to eq(:organization) + expect(district_sp.reason_for_not_being_selectable_by(user)).to eq(:organization) + end + + it "rejects selections when selecting is not allowed (via admin setting)" do + budget.phase = "on_hold" + expect(city_sp.reason_for_not_being_selectable_by(user)).to eq(:no_selecting_allowed) + expect(district_sp.reason_for_not_being_selectable_by(user)).to eq(:no_selecting_allowed) + end + + it "accepts valid selections when selecting is allowed" do + budget.phase = "selecting" + expect(city_sp.reason_for_not_being_selectable_by(user)).to be_nil + expect(district_sp.reason_for_not_being_selectable_by(user)).to be_nil + end + end + end + + describe "Order" do + describe "#sort_by_confidence_score" do + + it "should order by confidence_score" do + least_voted = create(:budget_investment, cached_votes_up: 1) + most_voted = create(:budget_investment, cached_votes_up: 10) + some_votes = create(:budget_investment, cached_votes_up: 5) + + expect(Budget::Investment.sort_by_confidence_score.first).to eq most_voted + expect(Budget::Investment.sort_by_confidence_score.second).to eq some_votes + expect(Budget::Investment.sort_by_confidence_score.third).to eq least_voted + end + + it "should order by confidence_score and then by id" do + least_voted = create(:budget_investment, cached_votes_up: 1) + most_voted = create(:budget_investment, cached_votes_up: 10) + most_voted2 = create(:budget_investment, cached_votes_up: 10) + least_voted2 = create(:budget_investment, cached_votes_up: 1) + + + expect(Budget::Investment.sort_by_confidence_score.first).to eq most_voted2 + expect(Budget::Investment.sort_by_confidence_score.second).to eq most_voted + expect(Budget::Investment.sort_by_confidence_score.third).to eq least_voted2 + expect(Budget::Investment.sort_by_confidence_score.fourth).to eq least_voted + end + end + end + + describe "responsible_name" do + let(:user) { create(:user, document_number: "123456") } + let!(:investment) { create(:budget_investment, author: user) } + + it "gets updated with the document_number" do + expect(investment.responsible_name).to eq("123456") + end + + it "does not get updated if the user is erased" do + user.erase + expect(user.document_number).to be_blank + investment.touch + expect(investment.responsible_name).to eq("123456") + end + end + + describe "total votes" do + it "takes into account physical votes in addition to web votes" do + b = create(:budget, :selecting) + sp = create(:budget_investment, budget: b) + + sp.register_selection(create(:user, :level_two)) + expect(sp.total_votes).to eq(1) + + sp.physical_votes = 10 + expect(sp.total_votes).to eq(11) + end + end + + describe "#with_supports" do + it "should return proposals with supports" do + sp1 = create(:budget_investment) + sp2 = create(:budget_investment) + create(:vote, votable: sp1) + + expect(Budget::Investment.with_supports).to include(sp1) + expect(Budget::Investment.with_supports).to_not include(sp2) + end + end + + describe "Final Voting" do + + describe 'Permissions' do + let(:budget) { create(:budget) } + let(:heading) { create(:budget_heading, budget: budget) } + let(:user) { create(:user, :level_two) } + let(:luser) { create(:user) } + let(:ballot) { create(:budget_ballot, budget: budget) } + let(:city_sp) { create(:budget_investment, budget: budget) } + let(:district_sp) { create(:budget_investment, budget: budget, heading: heading) } + + describe '#reason_for_not_being_ballotable_by' do + it "rejects not logged in users" do + expect(city_sp.reason_for_not_being_ballotable_by(nil, ballot)).to eq(:not_logged_in) + expect(district_sp.reason_for_not_being_ballotable_by(nil, ballot)).to eq(:not_logged_in) + end + + it "rejects not verified users" do + expect(city_sp.reason_for_not_being_ballotable_by(luser, ballot)).to eq(:not_verified) + expect(district_sp.reason_for_not_being_ballotable_by(luser, ballot)).to eq(:not_verified) + end + + it "rejects organizations" do + create(:organization, user: user) + expect(city_sp.reason_for_not_being_ballotable_by(user, ballot)).to eq(:organization) + expect(district_sp.reason_for_not_being_ballotable_by(user, ballot)).to eq(:organization) + end + + it "rejects votes when voting is not allowed (via admin setting)" do + budget.phase = "on_hold" + expect(city_sp.reason_for_not_being_ballotable_by(user, ballot)).to eq(:no_ballots_allowed) + expect(district_sp.reason_for_not_being_ballotable_by(user, ballot)).to eq(:no_ballots_allowed) + end + + it "accepts valid votes when voting is allowed" do + budget.phase = "balloting" + expect(city_sp.reason_for_not_being_ballotable_by(user, ballot)).to be_nil + expect(district_sp.reason_for_not_being_ballotable_by(user, ballot)).to be_nil + end + + xit "rejects city wide votes if no city money available" do + user.city_wide_investments_supported_count = 0 + expect(city_sp.reason_for_not_being_ballotable_by(user, ballot)).to eq(:no_city_supports_available) + end + + xit "rejects district wide votes if no district money available" do + user.district_wide_investments_supported_count = 0 + expect(district_sp.reason_for_not_being_ballotable_by(user, ballot)).to eq(:no_district_supports_available) + end + + xit "accepts valid district votes" do + expect(district_sp.reason_for_not_being_selectable_by(user)).to be_nil + user.supported_investments_geozone_id = district.id + expect(district_sp.reason_for_not_being_ballotable_by(user, ballot)).to be_nil + end + + xit "rejects users with different headings" do + budget.phase = "balloting" + california = create(:budget_heading, budget: budget) + new_york = create(:budget_heading, budget: budget) + + sp1 = create(:budget_investment, :feasible, heading: california, budget: budget) + sp2 = create(:budget_investment, :feasible, heading: new_york, budget: budget) + b = create(:budget_ballot, user: user, heading: california, investments: [sp1]) + + expect(sp2.reason_for_not_being_ballotable_by(user, b)).to eq(:different_heading_assigned) + end + + xit "rejects proposals with price higher than current available money" do + budget.phase = "balloting" + carabanchel = create(:budget_heading, budget: budget, price: 2000000) + sp1 = create(:budget_investment, :feasible, heading: carabanchel, price: 3000000, budget: budget) + sp2 = create(:budget_investment, :feasible, heading: carabanchel, price: 1000000, budget: budget) + b = create(:budget_ballot, user: user, heading: carabanchel, investments: [sp1]) + + expect(sp2.reason_for_not_being_ballotable_by(user, b)).to eq(:not_enough_money) + end + + end + + end + + end + +end From fc355224676c53f6cd18fbd9a30c3fd229cce1c5 Mon Sep 17 00:00:00 2001 From: kikito Date: Tue, 24 May 2016 13:25:39 +0200 Subject: [PATCH 08/13] uses a string in the factories --- spec/factories.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/factories.rb b/spec/factories.rb index 85047b511..5bebbeb38 100644 --- a/spec/factories.rb +++ b/spec/factories.rb @@ -212,7 +212,7 @@ FactoryGirl.define do price 1000000 end - factory :budget_investment, class: Budget::Investment do + factory :budget_investment, class: 'Budget::Investment' do sequence(:title) { |n| "Budget Investment #{n} title" } association :budget association :author, factory: :user From 9876583c1ab5ca2a08fe759c35c842a8a484635f Mon Sep 17 00:00:00 2001 From: kikito Date: Tue, 24 May 2016 15:31:23 +0200 Subject: [PATCH 09/13] deactivates test --- spec/models/budget/investment_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/models/budget/investment_spec.rb b/spec/models/budget/investment_spec.rb index d8563574e..2b61a917a 100644 --- a/spec/models/budget/investment_spec.rb +++ b/spec/models/budget/investment_spec.rb @@ -329,7 +329,7 @@ describe Budget::Investment do expect(district_sp.reason_for_not_being_ballotable_by(user, ballot)).to eq(:no_ballots_allowed) end - it "accepts valid votes when voting is allowed" do + xit "accepts valid votes when voting is allowed" do budget.phase = "balloting" expect(city_sp.reason_for_not_being_ballotable_by(user, ballot)).to be_nil expect(district_sp.reason_for_not_being_ballotable_by(user, ballot)).to be_nil From 2409cccd8ff8e9a7a1a95c90262830fef7d10ce9 Mon Sep 17 00:00:00 2001 From: kikito Date: Tue, 24 May 2016 17:38:41 +0200 Subject: [PATCH 10/13] Adds budget.price and ballot.heading_id --- .../20160524143107_add_heading_id_to_budget_ballot.rb | 6 ++++++ db/migrate/20160524144005_add_price_to_budget.rb | 5 +++++ db/schema.rb | 6 +++++- 3 files changed, 16 insertions(+), 1 deletion(-) create mode 100644 db/migrate/20160524143107_add_heading_id_to_budget_ballot.rb create mode 100644 db/migrate/20160524144005_add_price_to_budget.rb diff --git a/db/migrate/20160524143107_add_heading_id_to_budget_ballot.rb b/db/migrate/20160524143107_add_heading_id_to_budget_ballot.rb new file mode 100644 index 000000000..2bc254fbe --- /dev/null +++ b/db/migrate/20160524143107_add_heading_id_to_budget_ballot.rb @@ -0,0 +1,6 @@ +class AddHeadingIdToBudgetBallot < ActiveRecord::Migration + def change + add_column :budget_ballots, :heading_id, :integer + add_index :budget_ballots, :heading_id + end +end diff --git a/db/migrate/20160524144005_add_price_to_budget.rb b/db/migrate/20160524144005_add_price_to_budget.rb new file mode 100644 index 000000000..14c96dd7a --- /dev/null +++ b/db/migrate/20160524144005_add_price_to_budget.rb @@ -0,0 +1,5 @@ +class AddPriceToBudget < ActiveRecord::Migration + def change + add_column :budgets, :price, :integer + end +end diff --git a/db/schema.rb b/db/schema.rb index ed8e3ed58..04889b20a 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20160523164449) do +ActiveRecord::Schema.define(version: 20160524144005) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -78,8 +78,11 @@ ActiveRecord::Schema.define(version: 20160523164449) do t.integer "budget_id" t.datetime "created_at", null: false t.datetime "updated_at", null: false + t.integer "heading_id" end + add_index "budget_ballots", ["heading_id"], name: "index_budget_ballots_on_heading_id", using: :btree + create_table "budget_headings", force: :cascade do |t| t.integer "budget_id" t.integer "geozone_id" @@ -138,6 +141,7 @@ ActiveRecord::Schema.define(version: 20160523164449) do t.boolean "valuating", default: false t.datetime "created_at", null: false t.datetime "updated_at", null: false + t.integer "price" end create_table "campaigns", force: :cascade do |t| From dc686f1bf79b81f8689d91582d2fc0beeae46d87 Mon Sep 17 00:00:00 2001 From: kikito Date: Tue, 24 May 2016 17:39:15 +0200 Subject: [PATCH 11/13] Adds price to budget factory --- spec/factories.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/spec/factories.rb b/spec/factories.rb index 5bebbeb38..94ad15c0f 100644 --- a/spec/factories.rb +++ b/spec/factories.rb @@ -191,6 +191,7 @@ FactoryGirl.define do factory :budget do sequence(:name) { |n| "Budget #{n}" } currency_symbol "€" + price 10000 phase 'on_hold' trait :selecting do From 3e39272b3b3acf2e0083ced8c8957f404186b393 Mon Sep 17 00:00:00 2001 From: kikito Date: Tue, 24 May 2016 17:39:49 +0200 Subject: [PATCH 12/13] Fixes implementation of amount_available moving it to the ballot --- app/models/budget.rb | 6 +++--- app/models/budget/ballot.rb | 5 +++++ app/models/budget/investment.rb | 6 +++--- 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/app/models/budget.rb b/app/models/budget.rb index dc971bc36..b234a8b73 100644 --- a/app/models/budget.rb +++ b/app/models/budget.rb @@ -28,9 +28,9 @@ class Budget < ActiveRecord::Base phase == "finished" end - def amount_available(heading) - return 0 unless heading_ids.include?(heading.try(:id)) - heading.try(:price) || 10000 # FIXME + def heading_price(heading) + return price unless heading.present? + heading_ids.include?(heading.id) ? heading.price : -1 end end diff --git a/app/models/budget/ballot.rb b/app/models/budget/ballot.rb index 6dbd67aed..bf7851883 100644 --- a/app/models/budget/ballot.rb +++ b/app/models/budget/ballot.rb @@ -2,6 +2,7 @@ class Budget class Ballot < ActiveRecord::Base belongs_to :user belongs_to :budget + belongs_to :heading has_many :lines, dependent: :destroy has_many :investments, through: :lines @@ -13,5 +14,9 @@ class Budget def amount_spent(heading) investments.by_heading(heading).sum(:price).to_i end + + def amount_available(heading) + budget.heading_price(heading) - amount_spent(heading) + end end end diff --git a/app/models/budget/investment.rb b/app/models/budget/investment.rb index bc7c81072..32ea9ad92 100644 --- a/app/models/budget/investment.rb +++ b/app/models/budget/investment.rb @@ -151,7 +151,7 @@ class Budget return permission_problem(user) if permission_problem?(user) return :no_ballots_allowed unless budget.balloting? return :different_heading_assigned unless heading_id.blank? || ballot.blank? || heading_id == ballot.heading_id || ballot.heading_id.nil? - return :not_enough_money if ballot.present? && !enough_money? + return :not_enough_money if ballot.present? && !enough_money?(ballot) end def permission_problem(user) @@ -173,8 +173,8 @@ class Budget reason_for_not_being_ballotable_by(user).blank? end - def enough_money? - available_money = budget.amount_available(self.heading) + def enough_money?(ballot) + available_money = ballot.amount_available(self.heading) price.to_i <= available_money end From 11a5140cf0d989268a4f5bdf34eab8e358c113df Mon Sep 17 00:00:00 2001 From: kikito Date: Tue, 24 May 2016 17:40:07 +0200 Subject: [PATCH 13/13] Implements missing specs & removes deprecated ones --- spec/models/budget/investment_spec.rb | 33 ++++++++++----------------- 1 file changed, 12 insertions(+), 21 deletions(-) diff --git a/spec/models/budget/investment_spec.rb b/spec/models/budget/investment_spec.rb index 2b61a917a..eb4cab3ac 100644 --- a/spec/models/budget/investment_spec.rb +++ b/spec/models/budget/investment_spec.rb @@ -329,29 +329,20 @@ describe Budget::Investment do expect(district_sp.reason_for_not_being_ballotable_by(user, ballot)).to eq(:no_ballots_allowed) end - xit "accepts valid votes when voting is allowed" do + it "accepts valid ballots when voting is allowed" do budget.phase = "balloting" expect(city_sp.reason_for_not_being_ballotable_by(user, ballot)).to be_nil expect(district_sp.reason_for_not_being_ballotable_by(user, ballot)).to be_nil end - xit "rejects city wide votes if no city money available" do - user.city_wide_investments_supported_count = 0 - expect(city_sp.reason_for_not_being_ballotable_by(user, ballot)).to eq(:no_city_supports_available) - end - - xit "rejects district wide votes if no district money available" do - user.district_wide_investments_supported_count = 0 - expect(district_sp.reason_for_not_being_ballotable_by(user, ballot)).to eq(:no_district_supports_available) - end - - xit "accepts valid district votes" do + it "accepts valid district selections" do + budget.phase = "selecting" + expect(district_sp.reason_for_not_being_selectable_by(user)).to be_nil + ballot.heading_id = heading.id expect(district_sp.reason_for_not_being_selectable_by(user)).to be_nil - user.supported_investments_geozone_id = district.id - expect(district_sp.reason_for_not_being_ballotable_by(user, ballot)).to be_nil end - xit "rejects users with different headings" do + it "rejects users with different headings" do budget.phase = "balloting" california = create(:budget_heading, budget: budget) new_york = create(:budget_heading, budget: budget) @@ -363,14 +354,14 @@ describe Budget::Investment do expect(sp2.reason_for_not_being_ballotable_by(user, b)).to eq(:different_heading_assigned) end - xit "rejects proposals with price higher than current available money" do + it "rejects proposals with price higher than current available money" do budget.phase = "balloting" - carabanchel = create(:budget_heading, budget: budget, price: 2000000) - sp1 = create(:budget_investment, :feasible, heading: carabanchel, price: 3000000, budget: budget) - sp2 = create(:budget_investment, :feasible, heading: carabanchel, price: 1000000, budget: budget) - b = create(:budget_ballot, user: user, heading: carabanchel, investments: [sp1]) + carabanchel = create(:budget_heading, budget: budget, price: 35) + sp1 = create(:budget_investment, :feasible, heading: carabanchel, price: 30, budget: budget) + sp2 = create(:budget_investment, :feasible, heading: carabanchel, price: 10, budget: budget) + ballot = create(:budget_ballot, user: user, heading: carabanchel, investments: [sp1]) - expect(sp2.reason_for_not_being_ballotable_by(user, b)).to eq(:not_enough_money) + expect(sp2.reason_for_not_being_ballotable_by(user, ballot)).to eq(:not_enough_money) end end