Add approval voting to budgets

Co-Authored-By: Javi Martín <javim@elretirao.net>
This commit is contained in:
Ziyan Junaideen
2020-07-16 17:23:16 +02:00
committed by Javi Martín
parent 009c33d4e5
commit 1e3e8c1304
34 changed files with 368 additions and 38 deletions

View File

@@ -63,7 +63,7 @@ class Admin::BudgetHeadingsController < Admin::BaseController
end
def budget_heading_params
valid_attributes = [:price, :population, :allow_custom_content, :latitude, :longitude]
valid_attributes = [:price, :population, :allow_custom_content, :latitude, :longitude, :max_ballot_lines]
params.require(:budget_heading).permit(*valid_attributes, translation_params(Budget::Heading))
end
end

View File

@@ -70,6 +70,7 @@ class Admin::BudgetsController < Admin::BaseController
descriptions = Budget::Phase::PHASE_KINDS.map { |p| "description_#{p}" }.map(&:to_sym)
valid_attributes = [:phase,
:currency_symbol,
:voting_style,
administrator_ids: [],
valuator_ids: []
] + descriptions

View File

@@ -3,6 +3,12 @@ module BudgetsHelper
["balloting", "reviewing_ballots", "finished"].include? budget.phase
end
def budget_voting_styles_select_options
Budget::VOTING_STYLES.map do |style|
[Budget.human_attribute_name("voting_style_#{style}"), style]
end
end
def heading_name_and_price_html(heading, budget)
tag.div do
concat(heading.name + " ")

View File

@@ -20,11 +20,13 @@ class Budget < ApplicationRecord
end
CURRENCY_SYMBOLS = %w[€ $ £ ¥].freeze
VOTING_STYLES = %w[knapsack approval].freeze
validates_translation :name, presence: true
validates :phase, inclusion: { in: Budget::Phase::PHASE_KINDS }
validates :currency_symbol, presence: true
validates :slug, presence: true, format: /\A[a-z0-9\-_]+\z/
validates :voting_style, inclusion: { in: VOTING_STYLES }
has_many :investments, dependent: :destroy
has_many :ballots, dependent: :destroy
@@ -196,6 +198,10 @@ class Budget < ApplicationRecord
investments.winners.map(&:milestone_tag_list).flatten.uniq.sort
end
def approval_voting?
voting_style == "approval"
end
private
def generate_phases

View File

@@ -63,7 +63,7 @@ class Budget
def voting_style
@voting_style ||= voting_style_class.new(self)
end
delegate :amount_available, :amount_available_info, :amount_spent, :amount_spent_info,
delegate :amount_available, :amount_available_info, :amount_spent, :amount_spent_info, :amount_limit,
:amount_limit_info, :change_vote_info, :enough_resources?, :formatted_amount_available,
:formatted_amount_limit, :formatted_amount_spent, :not_enough_resources_error,
:percentage_spent, :reason_for_not_being_ballotable, :voted_info,
@@ -72,7 +72,7 @@ class Budget
private
def voting_style_class
Budget::VotingStyles::Knapsack
"Budget::VotingStyles::#{budget.voting_style.camelize}".constantize
end
end
end

View File

@@ -35,6 +35,7 @@ class Budget
format: /\A(-|\+)?([1-8]?\d(?:\.\d{1,})?|90(?:\.0{1,6})?)\z/
validates :longitude, length: { maximum: 22 }, allow_blank: true, \
format: /\A(-|\+)?((?:1[0-7]|[1-9])?\d(?:\.\d{1,})?|180(?:\.0{1,})?)\z/
validates :max_ballot_lines, numericality: { greater_than_or_equal_to: 1 }
delegate :budget, :budget_id, to: :group, allow_nil: true

View File

@@ -0,0 +1,25 @@
class Budget::VotingStyles::Approval < Budget::VotingStyles::Base
def enough_resources?(investment)
amount_available(investment.heading) > 0
end
def reason_for_not_being_ballotable(investment)
:not_enough_available_votes unless enough_resources?(investment)
end
def not_enough_resources_error
"insufficient votes"
end
def amount_spent(heading)
investments(heading).count
end
def amount_limit(heading)
heading.max_ballot_lines
end
def format(amount)
amount
end
end

View File

@@ -21,23 +21,39 @@ class Budget::VotingStyles::Base
def amount_available_info(heading)
I18n.t("budgets.ballots.show.amount_available.#{name}",
amount: formatted_amount_available(heading))
count: formatted_amount_available(heading))
end
def amount_spent_info(heading)
I18n.t("budgets.ballots.show.amount_spent.#{name}",
amount: formatted_amount_spent(heading))
count: formatted_amount_spent(heading))
end
def amount_limit_info(heading)
I18n.t("budgets.ballots.show.amount_limit.#{name}",
amount: formatted_amount_limit(heading))
count: formatted_amount_limit(heading))
end
def amount_available(heading)
amount_limit(heading) - amount_spent(heading)
end
def percentage_spent(heading)
100.0 * amount_spent(heading) / amount_limit(heading)
end
def formatted_amount_available(heading)
format(amount_available(heading))
end
def formatted_amount_spent(heading)
format(amount_spent(heading))
end
def formatted_amount_limit(heading)
format(amount_limit(heading))
end
private
def investments(heading)

View File

@@ -11,10 +11,6 @@ class Budget::VotingStyles::Knapsack < Budget::VotingStyles::Base
"insufficient funds"
end
def amount_available(heading)
amount_limit(heading) - amount_spent(heading)
end
def amount_spent(heading)
investments_price(heading)
end
@@ -23,18 +19,6 @@ class Budget::VotingStyles::Knapsack < Budget::VotingStyles::Base
ballot.budget.heading_price(heading)
end
def formatted_amount_available(heading)
format(amount_available(heading))
end
def formatted_amount_spent(heading)
format(amount_spent(heading))
end
def formatted_amount_limit(heading)
format(amount_limit(heading))
end
def format(amount)
ballot.budget.formatted_amount(amount)
end

View File

@@ -16,6 +16,12 @@
<div class="small-12 medium-6 column">
<%= f.text_field :price, maxlength: 8 %>
<% if @heading.budget.approval_voting? %>
<%= f.number_field :max_ballot_lines,
hint: t("admin.budget_headings.form.max_ballot_lines_info") %>
<% end %>
<%= f.text_field :population,
maxlength: 8,
data: { toggle_focus: "population-info" },

View File

@@ -13,6 +13,9 @@
<tr id="<%= dom_id(@group) %>">
<th><%= Budget::Heading.human_attribute_name(:name) %></th>
<th><%= Budget::Heading.human_attribute_name(:price) %></th>
<% if @budget.approval_voting? %>
<th><%= Budget::Heading.human_attribute_name(:max_ballot_lines) %></th>
<% end %>
<th><%= Budget::Heading.human_attribute_name(:population) %></th>
<th><%= Budget::Heading.human_attribute_name(:allow_custom_content) %></th>
<th><%= t("admin.actions.actions") %></th>
@@ -23,6 +26,9 @@
<tr id="<%= dom_id(heading) %>" class="heading">
<td><%= link_to heading.name, edit_admin_budget_group_heading_path(@budget, @group, heading) %></td>
<td><%= @budget.formatted_heading_price(heading) %></td>
<% if @budget.approval_voting? %>
<td><%= heading.max_ballot_lines %></td>
<% end %>
<td><%= heading.population %></td>
<td>
<%= heading.allow_custom_content ? t("admin.shared.true_value") : t("admin.shared.false_value") %>

View File

@@ -16,7 +16,12 @@
<div class="small-12 medium-6 column">
<%= f.select :phase, budget_phases_select_options %>
</div>
<div class="small-12 medium-3 column end">
<div class="small-12 medium-4 column">
<%= f.select :voting_style, budget_voting_styles_select_options %>
</div>
<div class="small-12 medium-2 column end">
<%= f.select :currency_symbol, budget_currency_symbol_select_options %>
</div>
</div>

View File

@@ -10,9 +10,5 @@
</div>
<p id="amount-spent" class="spent-amount-text" style="width: <%= ballot.percentage_spent(heading) %>%">
<small><%= t("budgets.progress_bar.assigned") %></small><%= ballot.formatted_amount_spent(heading) %>
<span id="amount-available" class="amount-available">
<small><%= t("budgets.progress_bar.available") %></small>
<span><%= ballot.formatted_amount_available(heading) %></span>
</span>
<%= render "budgets/ballot/progress_bar/#{ballot.budget.voting_style}", ballot: ballot, heading: heading %>
</p>

View File

@@ -0,0 +1,3 @@
<%= sanitize(t("budgets.progress_bar.votes",
count: ballot.amount_spent(heading),
limit: ballot.amount_limit(heading))) %>

View File

@@ -0,0 +1,5 @@
<small><%= t("budgets.progress_bar.assigned") %></small><%= ballot.formatted_amount_spent(heading) %>
<span id="amount-available" class="amount-available">
<small><%= t("budgets.progress_bar.available") %></small>
<span><%= ballot.formatted_amount_available(heading) %></span>
</span>

View File

@@ -127,6 +127,9 @@ ignore_unused:
- "budgets.phase.*"
- "budgets.investments.index.orders.*"
- "budgets.index.section_header.*"
- "budgets.ballots.show.amount_available.*"
- "budgets.ballots.show.amount_limit.*"
- "budgets.ballots.show.amount_spent.*"
- "budgets.investments.index.sidebar.voted_info.*"
- "activerecord.*"
- "activemodel.*"

View File

@@ -141,6 +141,9 @@ en:
description_finished: "Description when the budget is finished"
phase: "Phase"
currency_symbol: "Currency"
voting_style: "Final voting style"
voting_style_knapsack: "Knapsack"
voting_style_approval: "Approval"
budget/translation:
name: "Name"
budget/investment:
@@ -203,6 +206,7 @@ en:
name: "Heading name"
price: "Amount"
population: "Population (optional)"
max_ballot_lines: "Votes allowed"
budget/heading/translation:
name: "Heading name"
budget/phase:

View File

@@ -149,6 +149,7 @@ en:
success_notice: "Heading deleted successfully"
unable_notice: "You cannot delete a Heading that has associated investments"
form:
max_ballot_lines_info: 'Maximum number of projects a user can vote on this heading during the "Voting projects" phase. Only for budgets using approval voting.'
population_info: "Budget Heading population field is used for Statistic purposes at the end of the Budget to show for each Heading that represents an area with population what percentage voted. The field is optional so you can leave it empty if it doesn't apply."
coordinates_info: "If latitude and longitude are provided, the investments page for this heading will include a map. This map will be centered using those coordinates."
content_blocks_info: "If allow content block is checked, you will be able to create custom content related to this heading from the section Settings > Custom content blocks. This content will appear on the investments page for this heading."

View File

@@ -4,11 +4,22 @@ en:
show:
title: Your ballot
amount_available:
knapsack: "You still have <span>%{amount}</span> to invest."
knapsack: "You still have <span>%{count}</span> to invest."
approval:
zero: "You can still cast <span>%{count}</span> votes."
one: "You can still cast <span>%{count}</span> vote."
other: "You can still cast <span>%{count}</span> votes."
amount_spent:
knapsack: "Amount spent <span>%{amount}</span>"
knapsack: "Amount spent <span>%{count}</span>"
approval:
zero: "Votes cast: <span>%{count}</span>"
one: "Votes cast: <span>%{count}</span>"
other: "Votes cast: <span>%{count}</span>"
amount_limit:
knapsack: "%{amount}"
knapsack: "%{count}"
approval:
one: "You can vote <span>1</span> project"
other: "You can vote up to <span>%{count}</span> projects"
no_balloted_group_yet: "You have not voted on this group yet, go vote!"
remove: Remove vote
voted:
@@ -25,6 +36,7 @@ en:
not_enough_money: "You have already assigned the available budget.<br><small>Remember you can %{change_ballot} at any time</small>"
no_ballots_allowed: Selecting phase is closed
different_heading_assigned: "You have already voted a different heading: %{heading_link}"
not_enough_available_votes: "You have reached the maximum number of votes allowed"
change_ballot: change your votes
casted_offline: You have already participated offline
groups:
@@ -92,8 +104,12 @@ en:
knapsack:
one: "<strong>You voted one proposal with a cost of %{amount_spent}</strong>"
other: "<strong>You voted %{count} proposals with a cost of %{amount_spent}</strong>"
approval:
one: "<strong>You voted one proposal</strong>"
other: "<strong>You voted %{count} proposals</strong>"
change_vote_info:
knapsack: "You can %{link} at any time until the close of this phase. No need to spend all the money available."
approval: "You can %{link} at any time until the close of this phase."
change_vote_link: "change your vote"
different_heading_assigned: "You have active votes in another heading: %{heading_link}"
change_ballot: "If your change your mind you can remove your votes in %{check_ballot} and start again."
@@ -154,6 +170,10 @@ en:
progress_bar:
assigned: "You have assigned: "
available: "Available budget: "
votes:
zero: "You have selected <strong>0</strong> projects out of <strong>%{limit}</strong>"
one: "You have selected <strong>1</strong> project out of <strong>%{limit}</strong>"
other: "You have selected <strong>%{count}</strong> projects out of <strong>%{limit}</strong>"
show:
group: Group
phase: Actual phase

View File

@@ -143,6 +143,9 @@ es:
description_finished: "Descripción cuando el presupuesto ha finalizado / Resultados"
phase: "Fase"
currency_symbol: "Divisa"
voting_style: "Estilo de la votación final"
voting_style_knapsack: Bolsa de dinero
voting_style_approval: Por aprobación
budget/translation:
name: "Nombre"
budget/investment:
@@ -205,6 +208,7 @@ es:
name: "Nombre de la partida"
price: "Cantidad"
population: "Población (opcional)"
max_ballot_lines: "Votos permitidos"
budget/heading/translation:
name: "Nombre de la partida"
budget/phase:

View File

@@ -149,6 +149,7 @@ es:
success_notice: "Partida presupuestaria eliminada correctamente"
unable_notice: "No se puede eliminar una partida presupuestaria con proyectos asociados"
form:
max_ballot_lines_info: 'Máximo número de proyectos que un usuario puede votar en esta partida durante la fase "Votación final". Solamente se aplica a presupuestos con votación por aprobación.'
population_info: "El campo población de las partidas presupuestarias se usa con fines estadísticos únicamente, con el objetivo de mostrar el porcentaje de votos habidos en cada partida que represente un área con población. Es un campo opcional, así que puedes dejarlo en blanco si no aplica."
coordinates_info: "Si se añaden los campos latitud y longitud, en la página de proyectos de esta partida aparecerá un mapa, que estará centrado en esas coordenadas."
content_blocks_info: "Si se permite el bloque de contenidos, se tendrá la oportunidad de crear bloques de contenido relativos a esta partida desde la sección Configuración > Personalizar bloques. Este contenido aparecerá en la página de proyectos de esta partida."

View File

@@ -4,11 +4,22 @@ es:
show:
title: Mis votos
amount_available:
knapsack: "Te quedan <span>%{amount}</span> para invertir"
knapsack: "Te quedan <span>%{count}</span> para invertir"
approval:
zero: "Te quedan <span>0</span> votos disponibles"
one: "Te queda <span>1</span> voto disponible"
other: "Te quedan <span>%{count}</span> votos disponibles"
amount_spent:
knapsack: "Coste total <span>%{amount}</span>"
knapsack: "Coste total <span>%{count}</span>"
approval:
zero: "Votos: <span>%{count}</span>"
one: "Votos: <span>%{count}</span>"
other: "Votos: <span>%{count}</span>"
amount_limit:
knapsack: "%{amount}"
knapsack: "%{count}"
approval:
one: "Puedes votar <span>1</span> proyecto"
other: "Puedes votar hasta <span>%{count}</span> proyectos"
no_balloted_group_yet: "Todavía no has votado proyectos de este grupo, ¡vota!"
remove: Quitar voto
voted:
@@ -25,6 +36,7 @@ es:
not_enough_money: "Ya has asignado el presupuesto disponible.<br><small>Recuerda que puedes %{change_ballot} en cualquier momento</small>"
no_ballots_allowed: El periodo de votación está cerrado.
different_heading_assigned: "Ya has votado proyectos de otra partida: %{heading_link}"
not_enough_available_votes: "No tienes más votos disponibles"
change_ballot: cambiar tus votos
casted_offline: Ya has participado presencialmente
groups:
@@ -92,8 +104,12 @@ es:
knapsack:
one: "<strong>Has votado un proyecto por un valor de %{amount_spent}</strong>"
other: "<strong>Has votado %{count} proyectos por un valor de %{amount_spent}</strong>"
approval:
one: "<strong>Has votado un proyecto</strong>"
other: "<strong>Has votado %{count} proyectos</strong>"
change_vote_info:
knapsack: "Puedes %{link} en cualquier momento hasta el cierre de esta fase. No hace falta que gastes todo el dinero disponible."
approval: "Puedes %{link} en cualquier momento hasta el cierre de esta fase."
change_vote_link: "cambiar tus votos"
different_heading_assigned: "Ya apoyaste proyectos de otra sección del presupuesto: %{heading_link}"
change_ballot: "Si cambias de opinión puedes borrar tus votos en %{check_ballot} y volver a empezar."
@@ -154,6 +170,10 @@ es:
progress_bar:
assigned: "Has asignado: "
available: "Presupuesto disponible: "
votes:
zero: "Has seleccionado <strong>0</strong> proyectos de <strong>%{limit}</strong>"
one: "Has seleccionado <strong>1</strong> proyecto de <strong>%{limit}</strong>"
other: "Has seleccionado <strong>%{count}</strong> proyectos de <strong>%{limit}</strong>"
show:
group: Grupo
phase: Fase actual

View File

@@ -0,0 +1,6 @@
class AddApprovalVotingFields < ActiveRecord::Migration[4.2]
def change
add_column :budgets, :voting_style, :string, default: "knapsack"
add_column :budget_headings, :max_ballot_lines, :integer, default: 1
end
end

View File

@@ -214,6 +214,7 @@ ActiveRecord::Schema.define(version: 20200519120717) do
t.boolean "allow_custom_content", default: false
t.text "latitude"
t.text "longitude"
t.integer "max_ballot_lines", default: 1
t.datetime "created_at"
t.datetime "updated_at"
t.index ["group_id"], name: "index_budget_headings_on_group_id"
@@ -360,6 +361,7 @@ ActiveRecord::Schema.define(version: 20200519120717) do
t.text "description_drafting"
t.text "description_publishing_prices"
t.text "description_informing"
t.string "voting_style", default: "knapsack"
end
create_table "campaigns", id: :serial, force: :cascade do |t|

View File

@@ -55,6 +55,14 @@ FactoryBot.define do
results_enabled { true }
stats_enabled { true }
end
trait :knapsack do
voting_style { "knapsack" }
end
trait :approval do
voting_style { "approval" }
end
end
factory :budget_group, class: "Budget::Group" do

View File

@@ -0,0 +1,14 @@
require "rails_helper"
describe BudgetsHelper do
describe "#budget_voting_styles_select_options" do
it "provides vote kinds" do
types = [
["Knapsack", "knapsack"],
["Approval", "approval"]
]
expect(budget_voting_styles_select_options).to eq(types)
end
end
end

View File

@@ -40,6 +40,41 @@ describe Budget::Ballot::Line do
end
end
describe "Approval voting" do
before do
budget.update!(voting_style: "approval")
heading.update!(max_ballot_lines: 1)
end
it "is valid if there are votes left" do
expect(ballot_line).to be_valid
end
it "is not valid if there are no votes left" do
create(:budget_ballot_line, ballot: ballot,
investment: create(:budget_investment, :selected, heading: heading))
expect(ballot_line).not_to be_valid
end
it "is valid if insufficient funds but enough votes" do
investment.update!(price: heading.price + 1)
expect(ballot_line).to be_valid
end
it "validates votes when creating lines at the same time", :race_condition do
other_investment = create(:budget_investment, :selected, heading: heading)
other_line = build(:budget_ballot_line, ballot: ballot, investment: other_investment)
[ballot_line, other_line].map do |line|
Thread.new { line.save }
end.each(&:join)
expect(Budget::Ballot::Line.count).to be 1
end
end
describe "Selectibility" do
it "is not valid if investment is unselected" do
investment.update!(selected: false)

View File

@@ -65,6 +65,20 @@ describe Budget::Ballot do
expect(ballot.amount_spent(heading1)).to eq 50000
expect(ballot.amount_spent(heading2)).to eq 20000
end
it "returns the votes cast on a specific heading for approval voting" do
budget = create(:budget, :approval)
heading1 = create(:budget_heading, budget: budget, max_ballot_lines: 2)
heading2 = create(:budget_heading, budget: budget, max_ballot_lines: 3)
ballot = create(:budget_ballot, budget: budget)
ballot.investments << create(:budget_investment, :selected, heading: heading1)
ballot.investments << create(:budget_investment, :selected, heading: heading1)
ballot.investments << create(:budget_investment, :selected, heading: heading2)
expect(ballot.amount_spent(heading1)).to eq 2
expect(ballot.amount_spent(heading2)).to eq 1
end
end
describe "#amount_available" do
@@ -91,6 +105,20 @@ describe Budget::Ballot do
expect(ballot.amount_available(heading1)).to eq 500
end
it "returns the amount of votes left for approval voting" do
budget = create(:budget, :approval)
heading1 = create(:budget_heading, budget: budget, max_ballot_lines: 2)
heading2 = create(:budget_heading, budget: budget, max_ballot_lines: 3)
ballot = create(:budget_ballot, budget: budget)
ballot.investments << create(:budget_investment, :selected, heading: heading1)
ballot.investments << create(:budget_investment, :selected, heading: heading1)
ballot.investments << create(:budget_investment, :selected, heading: heading2)
expect(ballot.amount_available(heading1)).to eq 0
expect(ballot.amount_available(heading2)).to eq 2
end
end
describe "#heading_for_group" do

View File

@@ -323,4 +323,13 @@ describe Budget::Heading do
expect(Budget::Heading.allow_custom_content).to eq [translated_heading]
end
end
describe "#max_ballot_lines" do
it "must be at least 1" do
expect(build(:budget_heading, max_ballot_lines: 1)).to be_valid
expect(build(:budget_heading, max_ballot_lines: 10)).to be_valid
expect(build(:budget_heading, max_ballot_lines: -1)).not_to be_valid
expect(build(:budget_heading, max_ballot_lines: 0)).not_to be_valid
end
end
end

View File

@@ -1086,6 +1086,31 @@ describe Budget::Investment do
expect(inv2.reason_for_not_being_ballotable_by(user, ballot)).to eq(:not_enough_money)
end
context "Approval voting" do
before { budget.update!(phase: "balloting", voting_style: "approval") }
let(:group) { create(:budget_group, budget: budget) }
it "does not reject investments based on available money" do
heading = create(:budget_heading, group: group, max_ballot_lines: 2)
inv1 = create(:budget_investment, :selected, heading: heading, price: heading.price)
inv2 = create(:budget_investment, :selected, heading: heading, price: heading.price)
ballot = create(:budget_ballot, user: user, budget: budget, investments: [inv1])
expect(inv2.reason_for_not_being_ballotable_by(user, ballot)).to be nil
end
it "rejects if not enough available votes" do
heading = create(:budget_heading, group: group, max_ballot_lines: 1)
inv1 = create(:budget_investment, :selected, heading: heading)
inv2 = create(:budget_investment, :selected, heading: heading)
ballot = create(:budget_ballot, user: user, budget: budget, investments: [inv1])
reason = inv2.reason_for_not_being_ballotable_by(user, ballot)
expect(reason).to eq(:not_enough_available_votes)
end
end
end
end
end

View File

@@ -355,4 +355,25 @@ describe Budget do
expect(budget.investments_milestone_tags).to eq(["tag1"])
end
end
describe "#voting_style" do
context "Validations" do
it { expect(build(:budget, :approval)).to be_valid }
it { expect(build(:budget, :knapsack)).to be_valid }
it { expect(build(:budget, voting_style: "Oups!")).not_to be_valid }
end
context "Related supportive methods" do
describe "#approval_voting?" do
it { expect(build(:budget, :approval).approval_voting?).to be true }
it { expect(build(:budget, :knapsack).approval_voting?).to be false }
end
end
context "Defaults" do
it "defaults to knapsack voting style" do
expect(build(:budget).voting_style).to eq "knapsack"
end
end
end
end

View File

@@ -171,6 +171,30 @@ describe "Admin budget headings" do
expect(page).to have_css(".is-invalid-label", text: "Amount")
expect(page).to have_content "can't be blank"
end
describe "Max votes is optional", :js do
scenario "do no show max_ballot_lines field for knapsack budgets" do
visit new_admin_budget_group_heading_path(budget, group)
expect(page).not_to have_field "Votes allowed"
end
scenario "create heading with max_ballot_lines for appoval budgets" do
budget.update!(voting_style: "approval")
visit new_admin_budget_group_heading_path(budget, group)
expect(page).to have_field "Votes allowed", with: 1
fill_in "Heading name", with: "All City"
fill_in "Amount", with: "1000"
fill_in "Votes allowed", with: 14
click_button "Create new heading"
expect(page).to have_content "Heading created successfully!"
within("tr", text: "All City") { expect(page).to have_content 14 }
end
end
end
context "Edit" do

View File

@@ -102,7 +102,7 @@ describe "Admin budgets" do
end
context "New" do
scenario "Create budget" do
scenario "Create budget - Knapsack voting (default)" do
visit admin_budgets_path
click_link "Create new budget"
@@ -113,6 +113,21 @@ describe "Admin budgets" do
expect(page).to have_content "New participatory budget created successfully!"
expect(page).to have_content "M30 - Summer campaign"
expect(Budget.last.voting_style).to eq "knapsack"
end
scenario "Create budget - Approval voting", :js do
visit admin_budgets_path
click_link "Create new budget"
fill_in "Name", with: "M30 - Summer campaign"
select "Accepting projects", from: "budget[phase]"
select "Approval", from: "Final voting style"
click_button "Create Budget"
expect(page).to have_content "New participatory budget created successfully!"
expect(page).to have_content "M30 - Summer campaign"
expect(Budget.last.voting_style).to eq "approval"
end
scenario "Name is mandatory" do

View File

@@ -1,9 +1,10 @@
require "rails_helper"
describe "Votes" do
describe "Investments" do
let(:manuela) { create(:user, verified_at: Time.current) }
let(:budget) { create(:budget, :selecting) }
let(:manuela) { create(:user, verified_at: Time.current) }
context "Investments - Knapsack" do
let(:budget) { create(:budget, phase: "selecting") }
let(:group) { create(:budget_group, budget: budget) }
let(:heading) { create(:budget_heading, group: group) }
@@ -194,4 +195,33 @@ describe "Votes" do
end
end
end
context "Investments - Approval" do
let(:budget) { create(:budget, :balloting, :approval) }
before { login_as(manuela) }
scenario "Budget limit is ignored", :js do
group = create(:budget_group, budget: budget)
heading = create(:budget_heading, group: group, max_ballot_lines: 2)
investment1 = create(:budget_investment, :selected, heading: heading, price: heading.price)
investment2 = create(:budget_investment, :selected, heading: heading, price: heading.price)
visit budget_investments_path(budget, heading_id: heading.id)
add_to_ballot(investment1.title)
expect(page).to have_content("Remove vote")
expect(page).to have_content("You have selected 1 project out of 2")
within(".budget-investment", text: investment2.title) do
find("div.ballot").hover
expect(page).not_to have_content("You have already assigned the available budget")
end
visit budget_ballot_path(budget)
expect(page).to have_content("you can change your vote at any time until this phase is closed")
end
end
end