Merge pull request #2858 from consul/backport_1545-budget_poll_ballot

Verify poll ballots
This commit is contained in:
Javier Martín
2019-04-11 12:10:39 +02:00
committed by GitHub
12 changed files with 280 additions and 2 deletions

View File

@@ -2,6 +2,7 @@ class Budget
class Ballot < ActiveRecord::Base class Ballot < ActiveRecord::Base
belongs_to :user belongs_to :user
belongs_to :budget belongs_to :budget
belongs_to :poll_ballot, class_name: "Poll::Ballot"
has_many :lines, dependent: :destroy has_many :lines, dependent: :destroy
has_many :investments, through: :lines has_many :investments, through: :lines

View File

@@ -16,6 +16,7 @@ class Budget
scope :by_investment, ->(investment_id) { where(investment_id: investment_id) } scope :by_investment, ->(investment_id) { where(investment_id: investment_id) }
before_validation :set_denormalized_ids before_validation :set_denormalized_ids
after_save :store_user_heading
def check_sufficient_funds def check_sufficient_funds
errors.add(:money, "insufficient funds") if ballot.amount_available(investment.heading) < investment.price.to_i 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.group_id ||= investment.try(:group_id)
self.budget_id ||= investment.try(:budget_id) self.budget_id ||= investment.try(:budget_id)
end end
def store_user_heading
ballot.user.update(balloted_heading_id: heading.id) unless ballot.physical == true
end
end end
end end
end end

36
app/models/poll/ballot.rb Normal file
View File

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

View File

@@ -1,6 +1,7 @@
class Poll::BallotSheet < ActiveRecord::Base class Poll::BallotSheet < ActiveRecord::Base
belongs_to :poll belongs_to :poll
belongs_to :officer_assignment belongs_to :officer_assignment
has_many :ballots, class_name: Poll::Ballot
validates :data, presence: true validates :data, presence: true
validates :poll_id, presence: true validates :poll_id, presence: true
@@ -9,4 +10,33 @@ class Poll::BallotSheet < ActiveRecord::Base
def author def author
officer_assignment.officer.name officer_assignment.officer.name
end 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 end

View File

@@ -0,0 +1,5 @@
class AddBallotedHeadingIdToUsers < ActiveRecord::Migration
def change
add_column :users, :balloted_heading_id, :integer, default: nil
end
end

View File

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

View File

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

View File

@@ -140,8 +140,10 @@ ActiveRecord::Schema.define(version: 20190205131722) do
create_table "budget_ballots", force: :cascade do |t| create_table "budget_ballots", force: :cascade do |t|
t.integer "user_id" t.integer "user_id"
t.integer "budget_id" t.integer "budget_id"
t.datetime "created_at", null: false t.datetime "created_at", null: false
t.datetime "updated_at", null: false t.datetime "updated_at", null: false
t.boolean "physical", default: false
t.integer "poll_ballot_id"
end end
create_table "budget_content_blocks", force: :cascade do |t| 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", ["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 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| create_table "poll_booth_assignments", force: :cascade do |t|
t.integer "booth_id" t.integer "booth_id"
t.integer "poll_id" t.integer "poll_id"
@@ -1456,6 +1466,7 @@ ActiveRecord::Schema.define(version: 20190205131722) do
t.boolean "created_from_signature", default: false t.boolean "created_from_signature", default: false
t.integer "failed_email_digests_count", default: 0 t.integer "failed_email_digests_count", default: 0
t.text "former_users_data_log", default: "" t.text "former_users_data_log", default: ""
t.integer "balloted_heading_id"
t.boolean "public_interests", default: false t.boolean "public_interests", default: false
t.boolean "recommended_debates", default: true t.boolean "recommended_debates", default: true
t.boolean "recommended_proposals", default: true t.boolean "recommended_proposals", default: true

View File

@@ -153,6 +153,11 @@ FactoryBot.define do
data "1234;9876;5678\n1000;2000;3000;9999" data "1234;9876;5678\n1000;2000;3000;9999"
end 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 factory :officing_residence, class: "Officing::Residence" do
user user
association :officer, factory: :poll_officer association :officer, factory: :poll_officer

View File

@@ -44,6 +44,19 @@ describe Budget::Ballot::Line do
end 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 "scopes" do
describe "by_investment" do describe "by_investment" do

View File

@@ -37,4 +37,24 @@ describe Poll::BallotSheet do
end 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 end

View File

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