Merge pull request #1129 from consul/budget-model-specs

Budget model specs
This commit is contained in:
Juanjo Bazán
2016-05-24 18:11:11 +02:00
15 changed files with 520 additions and 31 deletions

View File

@@ -1,5 +1,13 @@
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
has_many :headings
def on_hold?
phase == "on_hold"
end
@@ -20,4 +28,9 @@ class Budget < ActiveRecord::Base
phase == "finished"
end
end
def heading_price(heading)
return price unless heading.present?
heading_ids.include?(heading.id) ? heading.price : -1
end
end

View File

@@ -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

View File

@@ -10,12 +10,13 @@ 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
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
@@ -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) }
@@ -33,18 +34,18 @@ 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) }
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) }
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) }
@@ -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?(ballot)
end
def permission_problem(user)
@@ -173,8 +174,7 @@ class Budget
end
def enough_money?(ballot)
return true if ballot.blank?
available_money = ballot.amount_available(heading)
available_money = ballot.amount_available(self.heading)
price.to_i <= available_money
end

View File

@@ -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

View File

@@ -0,0 +1,5 @@
class AddBudgetIdToInvestments < ActiveRecord::Migration
def change
add_reference :budget_investments, :budget, index: true
end
end

View File

@@ -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

View File

@@ -0,0 +1,5 @@
class AddBudgetInvestmentsCountToValuators < ActiveRecord::Migration
def change
add_column :valuators, :budget_investments_count, :integer, default: 0
end
end

View File

@@ -0,0 +1,5 @@
class RenameBiValuationCount < ActiveRecord::Migration
def change
rename_column :budget_investments, :valuation_assignments_count, :valuator_assignments_count
end
end

View File

@@ -0,0 +1,5 @@
class AddResponsibleNameToBudgetInvestments < ActiveRecord::Migration
def change
add_column :budget_investments, :responsible_name, :string
end
end

View File

@@ -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

View File

@@ -0,0 +1,5 @@
class AddPriceToBudget < ActiveRecord::Migration
def change
add_column :budgets, :price, :integer
end
end

View File

@@ -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: 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: 20160523143320) 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"
@@ -93,31 +96,43 @@ 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
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", ["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|
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"
@@ -126,6 +141,7 @@ ActiveRecord::Schema.define(version: 20160523143320) 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|
@@ -511,6 +527,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

View File

@@ -1,5 +1,4 @@
FactoryGirl.define do
sequence(:document_number) { |n| "#{n.to_s.rjust(8, '0')}X" }
factory :user do
@@ -192,6 +191,20 @@ FactoryGirl.define do
factory :budget do
sequence(:name) { |n| "Budget #{n}" }
currency_symbol ""
price 10000
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
@@ -201,12 +214,17 @@ FactoryGirl.define do
end
factory :budget_investment, class: 'Budget::Investment' do
sequence(:title) { |n| "Investment #{n} title" }
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"
@@ -358,4 +376,6 @@ FactoryGirl.define do
sequence(:name) { |n| "District #{n}" }
census_code { '01' }
end
end

View File

@@ -0,0 +1,373 @@
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 = "<script>alert('danger');</script>"
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 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
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
end
it "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
it "rejects proposals with price higher than current available money" do
budget.phase = "balloting"
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, ballot)).to eq(:not_enough_money)
end
end
end
end
end

View File

@@ -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