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:
@@ -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
|
||||
|
||||
@@ -16,9 +16,9 @@ class Budget
|
||||
|
||||
private
|
||||
|
||||
def generate_slug?
|
||||
slug.nil? || budget.drafting?
|
||||
end
|
||||
def generate_slug?
|
||||
slug.nil? || budget.drafting?
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
class AddMaxVotableHeadingsToBudgetGroups < ActiveRecord::Migration
|
||||
def change
|
||||
add_column :budget_groups, :max_votable_headings, :integer, default: 1
|
||||
end
|
||||
end
|
||||
@@ -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"
|
||||
@@ -101,8 +101,9 @@ ActiveRecord::Schema.define(version: 20180220211105) do
|
||||
|
||||
create_table "budget_groups", force: :cascade do |t|
|
||||
t.integer "budget_id"
|
||||
t.string "name", limit: 50
|
||||
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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,9 +657,84 @@ 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
|
||||
describe "#sort_by_confidence_score" do
|
||||
|
||||
|
||||
Reference in New Issue
Block a user