adds Budget::Group model to group headings

many refactors through budget related models
This commit is contained in:
Juanjo Bazán
2016-06-09 18:00:06 +02:00
parent 3fae269c40
commit b5a6828e41
18 changed files with 156 additions and 110 deletions

View File

@@ -4,9 +4,11 @@ class Budget < ActiveRecord::Base
validates :phase, inclusion: { in: VALID_PHASES }
has_many :investments
has_many :ballots
has_many :headings
has_many :investments, dependent: :destroy
has_many :ballots, dependent: :destroy
has_many :groups, dependent: :destroy
has_many :headings, through: :groups
has_many :investments, through: :headings
def on_hold?
phase == "on_hold"
@@ -29,7 +31,6 @@ class Budget < ActiveRecord::Base
end
def heading_price(heading)
return price unless heading.present?
heading_ids.include?(heading.id) ? heading.price : -1
end
end

View File

@@ -16,7 +16,7 @@ class Budget
end
def amount_available(heading)
budget.heading_price(heading) - amount_spent(heading.try(:id))
budget.heading_price(heading) - amount_spent(heading.id)
end
end
end

View File

@@ -0,0 +1,10 @@
class Budget
class Group < ActiveRecord::Base
belongs_to :budget
has_many :headings, dependent: :destroy
validates :budget_id, presence: true
validates :name, presence: true
end
end

View File

@@ -1,11 +1,11 @@
class Budget
class Heading < ActiveRecord::Base
belongs_to :budget
belongs_to :group
belongs_to :geozone
has_many :investments
validates :budget_id, presence: true
validates :group_id, presence: true
validates :name, presence: true
validates :price, presence: true
end

View File

@@ -10,7 +10,6 @@ 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
@@ -43,7 +42,6 @@ class Budget
scope :with_supports, -> { where('cached_votes_up > 0') }
scope :by_heading, -> (heading_id) { where(heading_id: heading_id) }
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("budget_valuator_assignments.valuator_id = ?", valuator_id).joins(:valuator_assignments) }
@@ -61,8 +59,7 @@ class Budget
end
def self.scoped_filter(params, current_filter)
budget = Budget.find!(params[:budget_id])
results = self.by_budget(params[:budget_id])
results = budget.investments
if params[:max_for_no_heading].present? || params[:max_per_heading].present?
results = limit_results(results, budget, params[:max_per_heading].to_i, params[:max_for_no_heading].to_i)
end
@@ -118,6 +115,10 @@ class Budget
where(heading_id: heading == 'all' ? nil : heading.presence)
end
def budget
heading.group.budget
end
def undecided?
feasibility == "undecided"
end
@@ -139,7 +140,7 @@ class Budget
end
def code
"B#{budget_id}I#{id}"
"B#{budget.id}I#{id}"
end
def reason_for_not_being_selectable_by(user)

View File

@@ -300,18 +300,21 @@ puts "Creating Budgets"
(1..10).each do |i|
budget = Budget.create!(name: (Date.today.year - 10 + i).to_s,
description: "<p>#{Faker::Lorem.paragraphs.join('</p><p>')}</p>",
price: rand(1 .. 100) * 100000,
phase: %w{on_hold accepting selecting balloting finished}.sample,
valuating: [false, true].sample)
puts budget.name
puts " "
geozones = Geozone.reorder("RANDOM()").limit(10)
geozones.each do |geozone|
heading = budget.headings.create!(name: geozone.name,
geozone: geozone,
price: rand(1 .. 100) * 10000)
budget.headings << heading
print "heading.name "
(1..[1,2,3].sample).each do |i|
group = budget.groups.create!(name: Faker::StarWars.planet)
geozones = Geozone.reorder("RANDOM()").limit([2,5,6,7].sample)
geozones.each do |geozone|
group.headings << group.headings.create!(name: geozone.name,
geozone: geozone,
price: rand(1 .. 100) * 10000)
end
print "#{group.name} "
end
puts ""
end

View File

@@ -1,9 +1,9 @@
class CreateBudgetHeading < ActiveRecord::Migration
def change
create_table :budget_headings do |t|
t.references :budget
t.references :group, index: true
t.references :geozone
t.string :name, limit: 50
t.string :name, limit: 50
t.integer :price, limit: 8
end
end

View File

@@ -0,0 +1,10 @@
class CreateBudgetGroup < ActiveRecord::Migration
def change
create_table :budget_groups do |t|
t.references :budget
t.string :name, limit: 50
end
add_index :budget_groups, :budget_id
end
end

View File

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

View File

@@ -0,0 +1,5 @@
class RemoveBudgetIdFromInvestments < ActiveRecord::Migration
def change
remove_column :budget_investments, :budget_id, :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: 20160531102008) do
ActiveRecord::Schema.define(version: 20160609152026) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@@ -63,6 +63,21 @@ ActiveRecord::Schema.define(version: 20160531102008) do
add_index "annotations", ["legislation_id"], name: "index_annotations_on_legislation_id", using: :btree
add_index "annotations", ["user_id"], name: "index_annotations_on_user_id", using: :btree
create_table "banners", force: :cascade do |t|
t.string "title", limit: 80
t.string "description"
t.string "target_url"
t.string "style"
t.string "image"
t.date "post_started_at"
t.date "post_ended_at"
t.datetime "hidden_at"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end
add_index "banners", ["hidden_at"], name: "index_banners_on_hidden_at", using: :btree
create_table "budget_ballot_lines", force: :cascade do |t|
t.integer "ballot_id"
t.integer "investment_id"
@@ -83,13 +98,22 @@ ActiveRecord::Schema.define(version: 20160531102008) do
add_index "budget_ballots", ["heading_id"], name: "index_budget_ballots_on_heading_id", using: :btree
create_table "budget_headings", force: :cascade do |t|
create_table "budget_groups", force: :cascade do |t|
t.integer "budget_id"
t.string "name", limit: 50
end
add_index "budget_groups", ["budget_id"], name: "index_budget_groups_on_budget_id", using: :btree
create_table "budget_headings", force: :cascade do |t|
t.integer "group_id"
t.integer "geozone_id"
t.string "name", limit: 50
t.integer "price", limit: 8
end
add_index "budget_headings", ["group_id"], name: "index_budget_headings_on_group_id", using: :btree
create_table "budget_investments", force: :cascade do |t|
t.integer "author_id"
t.integer "administrator_id"
@@ -114,13 +138,11 @@ ActiveRecord::Schema.define(version: 20160531102008) do
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
@@ -141,24 +163,8 @@ ActiveRecord::Schema.define(version: 20160531102008) do
t.boolean "valuating", default: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.integer "price"
end
create_table "banners", force: :cascade do |t|
t.string "title", limit: 80
t.string "description"
t.string "target_url"
t.string "style"
t.string "image"
t.date "post_started_at"
t.date "post_ended_at"
t.datetime "hidden_at"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end
add_index "banners", ["hidden_at"], name: "index_banners_on_hidden_at", using: :btree
create_table "campaigns", force: :cascade do |t|
t.string "name"
t.string "track_id"
@@ -204,10 +210,10 @@ ActiveRecord::Schema.define(version: 20160531102008) do
t.string "visit_id"
t.datetime "hidden_at"
t.integer "flags_count", default: 0
t.datetime "ignored_flag_at"
t.integer "cached_votes_total", default: 0
t.integer "cached_votes_up", default: 0
t.integer "cached_votes_down", default: 0
t.datetime "ignored_flag_at"
t.integer "comments_count", default: 0
t.datetime "confirmed_hide_at"
t.integer "cached_anonymous_votes_total", default: 0

View File

@@ -191,7 +191,6 @@ FactoryGirl.define do
factory :budget do
sequence(:name) { |n| "Budget #{n}" }
currency_symbol ""
price 10000
phase 'on_hold'
trait :selecting do
@@ -207,15 +206,20 @@ FactoryGirl.define do
end
end
factory :budget_heading, class: 'Budget::Heading' do
factory :budget_group, class: 'Budget::Group' do
budget
sequence(:name) { |n| "Group #{n}" }
end
factory :budget_heading, class: 'Budget::Heading' do
association :group, factory: :budget_group
sequence(:name) { |n| "Heading #{n}" }
price 1000000
end
factory :budget_investment, class: 'Budget::Investment' do
sequence(:title) { |n| "Budget Investment #{n} title" }
association :budget
association :heading, factory: :budget_heading
association :author, factory: :user
description 'Spend money on this'
unfeasibility_explanation ''

View File

@@ -65,7 +65,13 @@ describe "Abilities::Administrator" do
it { should be_able_to(:update, Budget::Investment) }
it { should be_able_to(:hide, Budget::Investment) }
it { should be_able_to(:valuate, create(:budget_investment, budget: create(:budget, valuating: true))) }
it { should_not be_able_to(:valuate, create(:budget_investment, budget: create(:budget, valuating: false))) }
it { should be_able_to(:valuate, create(:budget_investment,
heading: create(:budget_heading,
group: create(:budget_group,
budget: create(:budget, valuating: true))))) }
it { should_not be_able_to(:valuate, create(:budget_investment,
heading: create(:budget_heading,
group: create(:budget_group,
budget: create(:budget, valuating: false))))) }
end

View File

@@ -12,9 +12,16 @@ describe "Abilities::Common" do
let(:accepting_budget) { create(:budget, phase: 'accepting') }
let(:selecting_budget) { create(:budget, phase: 'selecting') }
let(:balloting_budget) { create(:budget, phase: 'balloting') }
let(:investment_in_accepting_budget) { create(:budget_investment, budget: accepting_budget) }
let(:investment_in_selecting_budget) { create(:budget_investment, budget: selecting_budget) }
let(:investment_in_balloting_budget) { create(:budget_investment, budget: balloting_budget) }
let(:accepting_budget_group) { create(:budget_group, budget: accepting_budget) }
let(:selecting_budget_group) { create(:budget_group, budget: selecting_budget) }
let(:balloting_budget_group) { create(:budget_group, budget: balloting_budget) }
let(:accepting_budget_heading) { create(:budget_heading, group: accepting_budget_group) }
let(:selecting_budget_heading) { create(:budget_heading, group: selecting_budget_group) }
let(:balloting_budget_heading) { create(:budget_heading, group: balloting_budget_group) }
let(:investment_in_accepting_budget) { create(:budget_investment, heading: accepting_budget_heading) }
let(:investment_in_selecting_budget) { create(:budget_investment, heading: selecting_budget_heading) }
let(:investment_in_balloting_budget) { create(:budget_investment, heading: balloting_budget_heading) }
let(:ballot_in_accepting_budget) { create(:budget_ballot, budget: accepting_budget) }
let(:ballot_in_selecting_budget) { create(:budget_ballot, budget: selecting_budget) }
let(:ballot_in_balloting_budget) { create(:budget_ballot, budget: balloting_budget) }

View File

@@ -7,10 +7,16 @@ describe "Abilities::Valuator" do
let(:valuator) { create(:valuator) }
let(:non_assigned_investment) { create(:budget_investment) }
let(:assigned_investment) { create(:budget_investment, budget: create(:budget, valuating: true)) }
let(:assigned_investment) { create(:budget_investment,
heading: create(:budget_heading,
group: create(:budget_group,
budget: create(:budget, valuating: true)))) }
before(:each) { assigned_investment.valuators << valuator }
let(:assigned_investment_not_valuating) { create(:budget_investment, budget: create(:budget, valuating: false)) }
let(:assigned_investment_not_valuating) { create(:budget_investment,
heading: create(:budget_heading,
group: create(:budget_group,
budget: create(:budget, valuating: false)))) }
before(:each) { assigned_investment_not_valuating.valuators << valuator }
it { should be_able_to(:read, SpendingProposal) }

View File

@@ -21,18 +21,15 @@ describe Budget::Ballot do
heading = create(:budget_heading)
inv1 = create(:budget_investment, :feasible, price: 10000, heading: heading)
inv2 = create(:budget_investment, :feasible, price: 20000, heading: create(:budget_heading))
inv3 = create(:budget_investment, :feasible, price: 25000)
inv4 = create(:budget_investment, :feasible, price: 40000, heading: heading)
inv3 = create(:budget_investment, :feasible, price: 40000, heading: heading)
ballot = create(:budget_ballot)
ballot.investments << inv1
ballot.investments << inv2
ballot.investments << inv3
expect(ballot.amount_spent(heading.id)).to eq 10000
expect(ballot.amount_spent(nil)).to eq 25000
ballot.investments << inv4
ballot.investments << inv3
expect(ballot.amount_spent(heading.id)).to eq 50000
end
@@ -40,28 +37,22 @@ describe Budget::Ballot do
describe "#amount_available" do
it "returns how much is left after taking some investments" do
budget = create(:budget, price: 200000)
heading = create(:budget_heading, budget: budget)
inv1 = create(:budget_investment, :feasible, price: 10000, heading: heading)
inv2 = create(:budget_investment, :feasible, price: 20000, heading: create(:budget_heading))
inv3 = create(:budget_investment, :feasible, price: 25000)
inv4 = create(:budget_investment, :feasible, price: 40000, heading: heading)
inv1 = create(:budget_investment, :feasible, price: 10000)
inv2 = create(:budget_investment, :feasible, price: 20000)
budget = create(:budget)
group = create(:budget_group, budget: budget)
heading = create(:budget_heading, group: group, price: 1000)
inv1 = create(:budget_investment, :feasible, price: 100, heading: heading)
inv2 = create(:budget_investment, :feasible, price: 200, heading: create(:budget_heading))
inv3 = create(:budget_investment, :feasible, price: 400, heading: heading)
ballot = create(:budget_ballot, budget: budget)
ballot.investments << inv1
ballot.investments << inv2
expect(ballot.amount_available(heading)).to eq 1000000
expect(ballot.amount_available(nil)).to eq 170000
expect(ballot.amount_available(heading)).to eq 900
ballot.investments << inv3
ballot.investments << inv4
expect(ballot.amount_available(heading)).to eq 960000
expect(ballot.amount_available(nil)).to eq 145000
expect(ballot.amount_available(heading)).to eq 500
end
end

View File

@@ -190,38 +190,33 @@ describe Budget::Investment do
describe 'Permissions' do
let(:budget) { create(:budget) }
let(:heading) { create(:budget_heading, budget: budget) }
let(:group) { create(:budget_group, budget: budget) }
let(:heading) { create(:budget_heading, group: group) }
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) }
let(:district_sp) { create(:budget_investment, 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
@@ -274,7 +269,9 @@ describe Budget::Investment do
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)
g = create(:budget_group, budget: b)
h = create(:budget_heading, group: g)
sp = create(:budget_investment, heading: h)
sp.register_selection(create(:user, :level_two))
expect(sp.total_votes).to eq(1)
@@ -299,56 +296,52 @@ describe Budget::Investment do
describe 'Permissions' do
let(:budget) { create(:budget) }
let(:heading) { create(:budget_heading, budget: budget) }
let(:group) { create(:budget_group, budget: budget) }
let(:heading) { create(:budget_heading, group: group) }
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) }
let(:investment) { create(:budget_investment, 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)
expect(investment.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)
expect(investment.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)
expect(investment.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)
expect(investment.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
expect(investment.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
expect(investment.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
expect(investment.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)
group = create(:budget_group, budget: budget)
california = create(:budget_heading, group: group)
new_york = create(:budget_heading, group: group)
sp1 = create(:budget_investment, :feasible, heading: california, budget: budget)
sp2 = create(:budget_investment, :feasible, heading: new_york, budget: budget)
sp1 = create(:budget_investment, :feasible, heading: california)
sp2 = create(:budget_investment, :feasible, heading: new_york)
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)
@@ -356,9 +349,10 @@ describe Budget::Investment do
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)
distritos = create(:budget_group, budget: budget)
carabanchel = create(:budget_heading, group: distritos, price: 35)
sp1 = create(:budget_investment, :feasible, heading: carabanchel, price: 30)
sp2 = create(:budget_investment, :feasible, heading: carabanchel, price: 10)
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)

View File

@@ -33,14 +33,11 @@ describe Budget do
end
describe "heading_price" do
let(:budget) { create(:budget, price: 1000) }
it "returns the budget price if no heading is provided" do
expect(budget.heading_price(nil)).to eq(1000)
end
let(:budget) { create(:budget) }
let(:group) { create(:budget_group, budget: budget) }
it "returns the heading price if the heading provided is part of the budget" do
heading = create(:budget_heading, price: 100, budget: budget)
heading = create(:budget_heading, price: 100, group: group)
expect(budget.heading_price(heading)).to eq(100)
end