From 673cfc82493f7ed7f33638048f6d5e8a9cebfc04 Mon Sep 17 00:00:00 2001 From: kikito Date: Mon, 23 May 2016 19:51:02 +0200 Subject: [PATCH] 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