Merge pull request #2546 from consul/vote_in_multiple_headings

Allow supporting investments on more than one heading per group
This commit is contained in:
Raimond Garcia
2018-03-22 23:33:24 +01:00
committed by GitHub
17 changed files with 269 additions and 21 deletions

View File

@@ -17,7 +17,7 @@ class Admin::BudgetGroupsController < Admin::BaseController
private
def budget_group_params
params.require(:budget_group).permit(:name)
params.require(:budget_group).permit(:name, :max_votable_headings)
end
end

View File

@@ -231,21 +231,20 @@ class Budget
end
def valid_heading?(user)
!different_heading_assigned?(user)
voted_in?(heading, user) ||
can_vote_in_another_heading?(user)
end
def different_heading_assigned?(user)
other_heading_ids = group.heading_ids - [heading.id]
voted_in?(other_heading_ids, user)
def can_vote_in_another_heading?(user)
headings_voted_by_user(user).count < group.max_votable_headings
end
def voted_in?(heading_ids, user)
heading_ids.include? heading_voted_by_user?(user)
def headings_voted_by_user(user)
user.votes.for_budget_investments(budget.investments.where(group: group)).votables.map(&:heading_id).uniq
end
def heading_voted_by_user?(user)
user.votes.for_budget_investments(budget.investments.where(group: group))
.votables.map(&:heading_id).first
def voted_in?(heading, user)
headings_voted_by_user(user).include?(heading.id)
end
def ballotable_by?(user)

View File

@@ -8,6 +8,19 @@
maxlength: 50,
placeholder: t("admin.budgets.form.group"),
class: "input-group-field" %>
<% if group.persisted? %>
<div class="small-12 medium-6 large-4">
<%= f.label :name, t("admin.budgets.form.max_votable_headings") %>
<%= f.select :max_votable_headings,
(1..group.headings.count),
label: false,
placeholder: t("admin.budgets.form.max_votable_headings"),
class: "input-group-field" %>
</div>
<% end %>
<div class="input-group-button">
<%= f.submit button_title, class: "button success" %>
</div>

View File

@@ -19,7 +19,7 @@
title: t('budgets.investments.investment.support_title'),
method: "post",
remote: (current_user && current_user.voted_in_group?(investment.group) ? true : false),
data: (current_user && current_user.voted_in_group?(investment.group) ? nil : { confirm: t('budgets.investments.investment.confirm_group')} ),
data: (current_user && current_user.voted_in_group?(investment.group) ? nil : { confirm: t('budgets.investments.investment.confirm_group', count: investment.group.max_votable_headings)} ),
"aria-hidden" => css_for_aria_hidden(reason) do %>
<%= t("budgets.investments.investment.give_support") %>
<% end %>
@@ -31,6 +31,7 @@
<p>
<small>
<%= t("votes.budget_investments.#{reason}",
count: investment.group.max_votable_headings,
verify_account: link_to(t("votes.verify_account"), verification_path),
signin: link_to(t("votes.signin"), new_user_session_path),
signup: link_to(t("votes.signup"), new_user_registration_path)

View File

@@ -176,11 +176,11 @@ ignore_unused:
- 'admin.site_customization.pages.page.status_*'
- 'admin.legislation.processes.process.*'
- 'legislation.processes.index.*'
- 'votes.budget_investments.different_heading_assigned*'
# - '{devise,kaminari,will_paginate}.*'
# - 'simple_form.{yes,no}'
# - 'simple_form.{placeholders,hints,labels}.*'
# - 'simple_form.{error_notification,required}.:'
## Exclude these keys from the `i18n-tasks eq-base' report:
# ignore_eq_base:
# all:

View File

@@ -119,6 +119,7 @@ en:
table_amount: Amount
table_population: Population
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."
max_votable_headings: "Maxium number of headings in which a user can vote"
winners:
calculate: Calculate Winner Investments
calculated: Winners being calculated, it may take a minute.

View File

@@ -130,7 +130,9 @@ en:
already_added: You have already added this investment project
already_supported: You have already supported this investment project. Share it!
support_title: Support this project
confirm_group: "You can only support investments in one heading. If you continue you cannot change your decision. Are you sure?"
confirm_group:
one: "You can only support investments in %{count} district. If you continue you cannot change the election of your district. Are you sure?"
other: "You can only support investments in %{count} district. If you continue you cannot change the election of your district. Are you sure?"
supports:
one: 1 support
other: "%{count} supports"

View File

@@ -771,7 +771,9 @@ en:
organization: Organizations are not permitted to vote
unfeasible: Unfeasible investment projects can not be supported
not_voting_allowed: Voting phase is closed
different_heading_assigned: You can only support investment projects in one heading
different_heading_assigned:
one: "You can only support investment projects in %{count} district"
other: "You can only support investment projects in %{count} districts"
welcome:
debates:
description: For meeting, discussing and sharing the things that matter to us in our city.

View File

@@ -119,6 +119,7 @@ es:
table_amount: Cantidad
table_population: Població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."
max_votable_headings: "Máximo número de partidas en que un usuario puede votar"
winners:
calculate: Calcular propuestas ganadoras
calculated: Calculando ganadoras, puede tardar un minuto.

View File

@@ -130,7 +130,9 @@ es:
already_added: Ya has añadido este proyecto de gasto
already_supported: Ya has apoyado este proyecto de gasto. ¡Compártelo!
support_title: Apoyar este proyecto
confirm_group: "Sólo puedes apoyar proyectos de una partida. Si sigues adelante no podrás cambiar esta decisión. ¿Estás seguro?"
confirm_group:
one: "Sólo puedes apoyar proyectos en %{count} distritos. Si sigues adelante no podrás cambiar la elección de este distrito. ¿Estás seguro?"
other: "Sólo puedes apoyar proyectos en %{count} distritos. Si sigues adelante no podrás cambiar la elección de este distrito. ¿Estás seguro?"
supports:
zero: Sin apoyos
one: 1 apoyo

View File

@@ -770,7 +770,9 @@ es:
organization: Las organizaciones no pueden votar.
unfeasible: No se pueden votar propuestas inviables.
not_voting_allowed: El periodo de votación está cerrado.
different_heading_assigned: Sólo puedes apoyar proyectos de gasto de una partida
different_heading_assigned:
one: "Sólo puedes apoyar proyectos de gasto de %{count} distrito"
other: "Sólo puedes apoyar proyectos de gasto de %{count} distritos"
welcome:
debates:
description: Encontrarnos, debatir y compartir lo que nos parece importante en nuestra ciudad.

View File

@@ -0,0 +1,5 @@
class AddMaxVotableHeadingsToBudgetGroups < ActiveRecord::Migration
def change
add_column :budget_groups, :max_votable_headings, :integer, default: 1
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: 20180220211105) do
ActiveRecord::Schema.define(version: 20180320104823) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@@ -103,6 +103,7 @@ ActiveRecord::Schema.define(version: 20180220211105) do
t.integer "budget_id"
t.string "name", limit: 50
t.string "slug"
t.integer "max_votable_headings", default: 1
end
add_index "budget_groups", ["budget_id"], name: "index_budget_groups_on_budget_id", using: :btree

View File

@@ -65,4 +65,49 @@ feature 'Admin can change the groups name' do
expect(page).to have_content('has already been taken')
end
context "Maximum votable headings" do
background do
3.times { create(:budget_heading, group: group) }
end
scenario "Defaults to 1 heading per group", :js do
visit admin_budget_path(group.budget)
within("#budget_group_#{group.id}") do
click_link 'Edit group'
expect(page).to have_select('budget_group_max_votable_headings', selected: '1')
end
end
scenario "Update", :js do
visit admin_budget_path(group.budget)
within("#budget_group_#{group.id}") do
click_link 'Edit group'
select '2', from: 'budget_group_max_votable_headings'
click_button 'Save group'
end
visit admin_budget_path(group.budget)
within("#budget_group_#{group.id}") do
click_link 'Edit group'
expect(page).to have_select('budget_group_max_votable_headings', selected: '2')
end
end
scenario "Do not display maxium votable headings' select in new form", :js do
visit admin_budget_path(group.budget)
click_link 'Add new group'
expect(page).to have_field('budget_group_name')
expect(page).to_not have_field('budget_group_max_votable_headings')
end
end
end

View File

@@ -103,5 +103,74 @@ feature 'Votes' do
expect(page).not_to have_css("budget_investment_#{investment.id}_votes")
end
end
context "Voting in multiple headings of a single group" do
let(:new_york) { heading }
let(:san_francisco) { create(:budget_heading, group: group) }
let(:third_heading) { create(:budget_heading, group: group) }
let!(:new_york_investment) { create(:budget_investment, heading: new_york) }
let!(:san_francisco_investment) { create(:budget_investment, heading: san_francisco) }
let!(:third_heading_investment) { create(:budget_investment, heading: third_heading) }
background do
group.update(max_votable_headings: 2)
end
scenario "From Index", :js do
visit budget_investments_path(budget, heading_id: new_york.id)
within("#budget_investment_#{new_york_investment.id}") do
find('.in-favor a').click
expect(page).to have_content "1 support"
expect(page).to have_content "You have already supported this investment project. Share it!"
end
visit budget_investments_path(budget, heading_id: san_francisco.id)
within("#budget_investment_#{san_francisco_investment.id}") do
find('.in-favor a').click
expect(page).to have_content "1 support"
expect(page).to have_content "You have already supported this investment project. Share it!"
end
visit budget_investments_path(budget, heading_id: third_heading.id)
within("#budget_investment_#{third_heading_investment.id}") do
find('.in-favor a').click
expect(page).to have_content "You can only support investment projects in 2 districts"
expect(page).to_not have_content "1 support"
expect(page).to_not have_content "You have already supported this investment project. Share it!"
end
end
scenario "From show", :js do
visit budget_investment_path(budget, new_york_investment)
find('.in-favor a').click
expect(page).to have_content "1 support"
expect(page).to have_content "You have already supported this investment project. Share it!"
visit budget_investment_path(budget, san_francisco_investment)
find('.in-favor a').click
expect(page).to have_content "1 support"
expect(page).to have_content "You have already supported this investment project. Share it!"
visit budget_investment_path(budget, third_heading_investment)
find('.in-favor a').click
expect(page).to have_content "You can only support investment projects in 2 districts"
expect(page).to_not have_content "1 support"
expect(page).to_not have_content "You have already supported this investment project. Share it!"
end
end
end
end

View File

@@ -589,6 +589,36 @@ describe Budget::Investment do
expect(salamanca_investment.valid_heading?(user)).to eq(false)
end
it "accepts votes in multiple headings of the same group" do
group.update(max_votable_headings: 2)
carabanchel = create(:budget_heading, group: group)
salamanca = create(:budget_heading, group: group)
carabanchel_investment = create(:budget_investment, heading: carabanchel)
salamanca_investment = create(:budget_investment, heading: salamanca)
create(:vote, votable: carabanchel_investment, voter: user)
expect(salamanca_investment.valid_heading?(user)).to eq(true)
end
it "accepts votes in any heading previously voted in" do
group.update(max_votable_headings: 2)
carabanchel = create(:budget_heading, group: group)
salamanca = create(:budget_heading, group: group)
carabanchel_investment = create(:budget_investment, heading: carabanchel)
salamanca_investment = create(:budget_investment, heading: salamanca)
create(:vote, votable: carabanchel_investment, voter: user)
create(:vote, votable: salamanca_investment, voter: user)
expect(carabanchel_investment.valid_heading?(user)).to eq(true)
expect(salamanca_investment.valid_heading?(user)).to eq(true)
end
it "allows votes in a group with a single heading" do
all_city_investment = create(:budget_investment, heading: heading)
expect(all_city_investment.valid_heading?(user)).to eq(true)
@@ -627,7 +657,82 @@ describe Budget::Investment do
expect(carabanchel_investment.valid_heading?(user)).to eq(true)
end
describe "#can_vote_in_another_heading?" do
let(:districts) { create(:budget_group, budget: budget) }
let(:carabanchel) { create(:budget_heading, group: districts) }
let(:salamanca) { create(:budget_heading, group: districts) }
let(:latina) { create(:budget_heading, group: districts) }
let(:carabanchel_investment) { create(:budget_investment, heading: carabanchel) }
let(:salamanca_investment) { create(:budget_investment, heading: salamanca) }
let(:latina_investment) { create(:budget_investment, heading: latina) }
it "returns true if the user has voted in less headings than the maximum" do
districts.update(max_votable_headings: 2)
create(:vote, votable: carabanchel_investment, voter: user)
expect(salamanca_investment.can_vote_in_another_heading?(user)).to eq(true)
end
it "returns false if the user has already voted in the maximum number of headings" do
districts.update(max_votable_headings: 2)
create(:vote, votable: carabanchel_investment, voter: user)
create(:vote, votable: salamanca_investment, voter: user)
expect(latina_investment.can_vote_in_another_heading?(user)).to eq(false)
end
end
end
end
describe "#headings_voted_by_user" do
it "returns the headings voted by a user" do
user1 = create(:user)
user2 = create(:user)
budget = create(:budget)
group = create(:budget_group, budget: budget)
new_york = create(:budget_heading, group: group)
san_franciso = create(:budget_heading, group: group)
another_heading = create(:budget_heading, group: group)
new_york_investment = create(:budget_investment, heading: new_york)
san_franciso_investment = create(:budget_investment, heading: san_franciso)
another_investment = create(:budget_investment, heading: san_franciso)
create(:vote, votable: new_york_investment, voter: user1)
create(:vote, votable: san_franciso_investment, voter: user1)
expect(another_investment.headings_voted_by_user(user1)).to include(new_york.id)
expect(another_investment.headings_voted_by_user(user1)).to include(san_franciso.id)
expect(another_investment.headings_voted_by_user(user1)).to_not include(another_heading.id)
expect(another_investment.headings_voted_by_user(user2)).to_not include(new_york.id)
expect(another_investment.headings_voted_by_user(user2)).to_not include(san_franciso.id)
expect(another_investment.headings_voted_by_user(user2)).to_not include(another_heading.id)
end
end
describe "#voted_in?" do
let(:user) { create(:user) }
let(:investment) { create(:budget_investment) }
it "returns true if the user has voted in this heading" do
create(:vote, votable: investment, voter: user)
expect(investment.voted_in?(investment.heading, user)).to eq(true)
end
it "returns false if the user has not voted in this heading" do
expect(investment.voted_in?(investment.heading, user)).to eq(false)
end
end
describe "Order" do