diff --git a/app/models/budget/ballot.rb b/app/models/budget/ballot.rb index 7dc618360..b53e8b8e6 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 :poll_ballot, class_name: "Poll::Ballot" has_many :lines, dependent: :destroy has_many :investments, through: :lines diff --git a/app/models/budget/ballot/line.rb b/app/models/budget/ballot/line.rb index a3c24b631..2d75c1cd4 100644 --- a/app/models/budget/ballot/line.rb +++ b/app/models/budget/ballot/line.rb @@ -16,6 +16,7 @@ class Budget scope :by_investment, ->(investment_id) { where(investment_id: investment_id) } before_validation :set_denormalized_ids + after_save :store_user_heading def check_sufficient_funds errors.add(:money, "insufficient funds") if ballot.amount_available(investment.heading) < investment.price.to_i @@ -37,6 +38,10 @@ class Budget self.group_id ||= investment.try(:group_id) self.budget_id ||= investment.try(:budget_id) end + + def store_user_heading + ballot.user.update(balloted_heading_id: heading.id) unless ballot.physical == true + end end end end diff --git a/app/models/poll/ballot.rb b/app/models/poll/ballot.rb new file mode 100644 index 000000000..0a8a55d85 --- /dev/null +++ b/app/models/poll/ballot.rb @@ -0,0 +1,36 @@ +class Poll::Ballot < ActiveRecord::Base + belongs_to :ballot_sheet, class_name: Poll::BallotSheet + + validates :ballot_sheet_id, presence: true + + def verify + investments.each do |investment_id| + add_investment(investment_id) + end + end + + def add_investment(investment_id) + investment = find_investment(investment_id) + + if investment.present? && not_already_added?(investment) + ballot.add_investment(investment) + end + end + + def investments + data.split(",") + end + + def ballot + Budget::Ballot.where(poll_ballot: self).first + end + + def find_investment(investment_id) + ballot.budget.investments.where(id: investment_id).first + end + + def not_already_added?(investment) + ballot.lines.where(investment: investment).blank? + end + +end diff --git a/app/models/poll/ballot_sheet.rb b/app/models/poll/ballot_sheet.rb index 24c3c38b8..883cb004b 100644 --- a/app/models/poll/ballot_sheet.rb +++ b/app/models/poll/ballot_sheet.rb @@ -1,6 +1,7 @@ class Poll::BallotSheet < ActiveRecord::Base belongs_to :poll belongs_to :officer_assignment + has_many :ballots, class_name: Poll::Ballot validates :data, presence: true validates :poll_id, presence: true @@ -9,4 +10,33 @@ class Poll::BallotSheet < ActiveRecord::Base def author officer_assignment.officer.name end + + def verify_ballots + parsed_ballots.each_with_index do |investment_ids, index| + ballot = create_ballots(investment_ids, index) + ballot.verify + end + end + + def parsed_ballots + data.split(/[;\n]/) + end + + private + + def create_ballots(investment_ids, index) + poll_ballot = Poll::Ballot.where(ballot_sheet: self, + data: investment_ids, + external_id: index).first_or_create + create_ballot(poll_ballot) + poll_ballot + end + + def create_ballot(poll_ballot) + Budget::Ballot.where(physical: true, + user: nil, + poll_ballot: poll_ballot, + budget: poll.budget).first_or_create + end + end diff --git a/db/migrate/20170514192303_add_balloted_heading_id_to_users.rb b/db/migrate/20170514192303_add_balloted_heading_id_to_users.rb new file mode 100644 index 000000000..d1fcd04cb --- /dev/null +++ b/db/migrate/20170514192303_add_balloted_heading_id_to_users.rb @@ -0,0 +1,5 @@ +class AddBallotedHeadingIdToUsers < ActiveRecord::Migration + def change + add_column :users, :balloted_heading_id, :integer, default: nil + end +end diff --git a/db/migrate/20180704093831_create_poll_ballot.rb b/db/migrate/20180704093831_create_poll_ballot.rb new file mode 100644 index 000000000..137b71255 --- /dev/null +++ b/db/migrate/20180704093831_create_poll_ballot.rb @@ -0,0 +1,10 @@ +class CreatePollBallot < ActiveRecord::Migration + def change + create_table :poll_ballots do |t| + t.integer :ballot_sheet_id + t.text :data + t.integer :external_id + t.timestamps null: false + end + end +end diff --git a/db/migrate/20180704095538_add_physical_to_budget_ballot.rb b/db/migrate/20180704095538_add_physical_to_budget_ballot.rb new file mode 100644 index 000000000..a06889d1b --- /dev/null +++ b/db/migrate/20180704095538_add_physical_to_budget_ballot.rb @@ -0,0 +1,6 @@ +class AddPhysicalToBudgetBallot < ActiveRecord::Migration + def change + add_column :budget_ballots, :physical, :boolean, default: false + add_column :budget_ballots, :poll_ballot_id, :integer + end +end diff --git a/db/schema.rb b/db/schema.rb index d65c89471..61b5e923f 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -140,8 +140,10 @@ ActiveRecord::Schema.define(version: 20190205131722) do create_table "budget_ballots", force: :cascade do |t| t.integer "user_id" t.integer "budget_id" - 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.boolean "physical", default: false + t.integer "poll_ballot_id" end create_table "budget_content_blocks", force: :cascade do |t| @@ -938,6 +940,14 @@ ActiveRecord::Schema.define(version: 20190205131722) do add_index "poll_ballot_sheets", ["officer_assignment_id"], name: "index_poll_ballot_sheets_on_officer_assignment_id", using: :btree add_index "poll_ballot_sheets", ["poll_id"], name: "index_poll_ballot_sheets_on_poll_id", using: :btree + create_table "poll_ballots", force: :cascade do |t| + t.integer "ballot_sheet_id" + t.text "data" + t.integer "external_id" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + end + create_table "poll_booth_assignments", force: :cascade do |t| t.integer "booth_id" t.integer "poll_id" @@ -1456,6 +1466,7 @@ ActiveRecord::Schema.define(version: 20190205131722) do t.boolean "created_from_signature", default: false t.integer "failed_email_digests_count", default: 0 t.text "former_users_data_log", default: "" + t.integer "balloted_heading_id" t.boolean "public_interests", default: false t.boolean "recommended_debates", default: true t.boolean "recommended_proposals", default: true diff --git a/spec/factories/polls.rb b/spec/factories/polls.rb index 895d723a9..2dee887aa 100644 --- a/spec/factories/polls.rb +++ b/spec/factories/polls.rb @@ -153,6 +153,11 @@ FactoryBot.define do data "1234;9876;5678\n1000;2000;3000;9999" end + factory :poll_ballot, class: "Poll::Ballot" do + association :ballot_sheet, factory: :poll_ballot_sheet + data "1,2,3" + end + factory :officing_residence, class: "Officing::Residence" do user association :officer, factory: :poll_officer diff --git a/spec/models/budget/ballot/line_spec.rb b/spec/models/budget/ballot/line_spec.rb index e91d36416..e73d54046 100644 --- a/spec/models/budget/ballot/line_spec.rb +++ b/spec/models/budget/ballot/line_spec.rb @@ -44,6 +44,19 @@ describe Budget::Ballot::Line do end + describe "#store_user_heading" do + + it "stores the heading where the user has voted" do + user = create(:user, :level_two) + investment = create(:budget_investment, :selected) + ballot = create(:budget_ballot, user: user, budget: investment.budget) + + create(:budget_ballot_line, ballot: ballot, investment: investment) + + expect(user.balloted_heading_id).to eq(investment.heading.id) + end + end + describe "scopes" do describe "by_investment" do diff --git a/spec/models/poll/ballot_sheet_spec.rb b/spec/models/poll/ballot_sheet_spec.rb index 2ad0e0d5d..000a5a841 100644 --- a/spec/models/poll/ballot_sheet_spec.rb +++ b/spec/models/poll/ballot_sheet_spec.rb @@ -37,4 +37,24 @@ describe Poll::BallotSheet do end + describe "#verify_ballots" do + it "creates ballots for each document number" do + budget = create(:budget) + poll = create(:poll, budget: budget) + poll_ballot = create(:poll_ballot_sheet, poll: poll, data: "1,2,3;4,5,6") + poll_ballot.verify_ballots + + expect(Poll::Ballot.count).to eq(2) + expect(Budget::Ballot.count).to eq(2) + end + end + + describe "#parsed_ballots" do + it "splits ballots by ';' or '\n'" do + data = "1,2,3;4,5,6\n7,8,9" + ballot_sheet.update(data: data) + + expect(ballot_sheet.parsed_ballots).to eq(["1,2,3", "4,5,6", "7,8,9"]) + end + end end diff --git a/spec/models/poll/ballot_spec.rb b/spec/models/poll/ballot_spec.rb new file mode 100644 index 000000000..021bd53b6 --- /dev/null +++ b/spec/models/poll/ballot_spec.rb @@ -0,0 +1,136 @@ +require "rails_helper" + +describe Poll::Ballot do + + let(:budget){ create(:budget) } + let(:group){ create(:budget_group, budget: budget) } + let(:heading){ create(:budget_heading, group: group, price: 10000000) } + let(:investment){ create(:budget_investment, :selected, price: 5000000, heading: heading) } + let(:poll) { create(:poll, budget: budget) } + let(:poll_ballot_sheet) { create(:poll_ballot_sheet, poll: poll) } + let(:poll_ballot) { create(:poll_ballot, ballot_sheet: poll_ballot_sheet, external_id: 1, data: investment.id) } + let!(:ballot) { create(:budget_ballot, budget: budget, physical: true, poll_ballot: poll_ballot) } + + describe "#verify" do + + it "adds ballot lines until there are sufficiente funds" do + investment2 = create(:budget_investment, :selected, price: 2000000, heading: heading) + investment3 = create(:budget_investment, :selected, price: 2000000, heading: heading) + investment4 = create(:budget_investment, :selected, price: 2000000, heading: heading) + + poll_ballot.update(data: [investment.id, investment2.id, investment3.id, investment4.id].join(",")) + poll_ballot.verify + + expect(poll_ballot.ballot.lines.count).to eq(3) + expect(poll_ballot.ballot.lines.pluck(:investment_id).sort).to eq([investment.id, investment2.id, investment3.id].sort) + end + + it "adds ballot lines if they are from valid headings" do + other_heading = create(:budget_heading, group: group, price: 10000000) + + investment2 = create(:budget_investment, :selected, price: 2000000, heading: heading) + investment3 = create(:budget_investment, :selected, price: 2000000, heading: heading) + investment4 = create(:budget_investment, :selected, price: 2000000, heading: other_heading) + + poll_ballot.update(data: [investment.id, investment2.id, investment3.id, investment4.id].join(",")) + poll_ballot.verify + + expect(poll_ballot.ballot.lines.count).to eq(3) + expect(poll_ballot.ballot.lines.pluck(:investment_id).sort).to eq([investment.id, investment2.id, investment3.id].sort) + end + + it "adds ballot lines if they are from selectable" do + investment2 = create(:budget_investment, :selected, price: 2000000, heading: heading) + investment3 = create(:budget_investment, :selected, price: 2000000, heading: heading) + investment4 = create(:budget_investment, price: 2000000, heading: heading) + + poll_ballot.update(data: [investment.id, investment2.id, investment3.id, investment4.id].join(",")) + poll_ballot.verify + + expect(poll_ballot.ballot.lines.count).to eq(3) + expect(poll_ballot.ballot.lines.pluck(:investment_id).sort).to eq([investment.id, investment2.id, investment3.id].sort) + end + + end + + describe "#add_investment" do + + describe "Money" do + it "is not valid if insufficient funds" do + investment.update(price: heading.price + 1) + expect(poll_ballot.add_investment(investment.id)).to be(false) + end + + it "is valid if sufficient funds" do + investment.update(price: heading.price - 1) + expect(poll_ballot.add_investment(investment.id)).to be(true) + end + end + + describe "Heading" do + it "is not valid if investment heading is not valid" do + expect(poll_ballot.add_investment(investment.id)).to be(true) + + other_heading = create(:budget_heading, group: group, price: 10000000) + other_investment = create(:budget_investment, :selected, price: 1000000, heading: other_heading) + + expect(poll_ballot.add_investment(other_investment.id)).to be(false) + end + + it "is valid if investment heading is valid" do + expect(poll_ballot.add_investment(investment.id)).to be(true) + + other_investment = create(:budget_investment, :selected, price: 1000000, heading: heading) + + expect(poll_ballot.add_investment(other_investment.id)).to be(true) + end + end + + describe "Selectibility" do + it "is not valid if investment is unselected" do + investment.update(selected: false) + expect(poll_ballot.add_investment(investment.id)).to be(false) + end + + it "is valid if investment is selected" do + investment.update(selected: true, price: 20000) + expect(poll_ballot.add_investment(investment.id)).to be(true) + end + end + + describe "Budget" do + it "is not valid if investment belongs to a different budget" do + other_budget = create(:budget) + investment.update(budget: other_budget) + expect(poll_ballot.add_investment(investment.id)).to be(nil) + end + + it "is valid if investment belongs to the poll's budget" do + expect(poll_ballot.add_investment(investment.id)).to be(true) + end + end + + describe "Already added" do + it "is not valid if already exists" do + poll_ballot.add_investment(investment.id) + expect(poll_ballot.add_investment(investment.id)).to be(nil) + end + + it "is valid if does not already exist" do + expect(poll_ballot.add_investment(investment.id)).to be(true) + end + end + end + + describe "#find_investment" do + it "returns the investment if found" do + expect(poll_ballot.find_investment(investment.id)).to eq(investment) + end + + it "finds investments with trailing zeros" do + expect(poll_ballot.find_investment("0#{investment.id}")).to eq(investment) + expect(poll_ballot.find_investment("00#{investment.id}")).to eq(investment) + end + end + +end