@@ -312,6 +312,11 @@ body.admin {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[class^="icon-"].delete {
|
||||||
|
border: 0;
|
||||||
|
font-size: $base-font-size;
|
||||||
|
}
|
||||||
|
|
||||||
.verified {
|
.verified {
|
||||||
color: $check;
|
color: $check;
|
||||||
|
|
||||||
|
|||||||
@@ -234,6 +234,14 @@ a {
|
|||||||
color: $brand;
|
color: $brand;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.no-margin-top {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.no-padding-top {
|
||||||
|
padding-top: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.small {
|
.small {
|
||||||
@@ -1194,6 +1202,16 @@ table {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.table-for-mobile {
|
||||||
|
|
||||||
|
@include breakpoint(medium down) {
|
||||||
|
th, td {
|
||||||
|
display: block;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 12. Social
|
// 12. Social
|
||||||
// ----------
|
// ----------
|
||||||
|
|
||||||
|
|||||||
21
app/controllers/budgets/results_controller.rb
Normal file
21
app/controllers/budgets/results_controller.rb
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
module Budgets
|
||||||
|
class ResultsController < ApplicationController
|
||||||
|
|
||||||
|
load_and_authorize_resource :budget
|
||||||
|
|
||||||
|
def show
|
||||||
|
@result = load_result
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def load_result
|
||||||
|
Budget::Result.new(@budget, heading)
|
||||||
|
end
|
||||||
|
|
||||||
|
def heading
|
||||||
|
@budget.headings.find(params[:heading_id])
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -55,4 +55,8 @@ module ApplicationHelper
|
|||||||
def content_block(name, locale)
|
def content_block(name, locale)
|
||||||
SiteCustomization::ContentBlock.block_for(name, locale)
|
SiteCustomization::ContentBlock.block_for(name, locale)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def format_price(number)
|
||||||
|
number_to_currency(number, precision: 0, locale: I18n.default_locale)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ class Budget
|
|||||||
validates :terms_of_service, acceptance: { allow_nil: false }, on: :create
|
validates :terms_of_service, acceptance: { allow_nil: false }, on: :create
|
||||||
|
|
||||||
scope :sort_by_confidence_score, -> { reorder(confidence_score: :desc, id: :desc) }
|
scope :sort_by_confidence_score, -> { reorder(confidence_score: :desc, id: :desc) }
|
||||||
|
scope :sort_by_ballots, -> { reorder(ballot_lines_count: :desc, id: :desc) }
|
||||||
scope :sort_by_price, -> { reorder(price: :desc, confidence_score: :desc, id: :desc) }
|
scope :sort_by_price, -> { reorder(price: :desc, confidence_score: :desc, id: :desc) }
|
||||||
scope :sort_by_random, -> { reorder("RANDOM()") }
|
scope :sort_by_random, -> { reorder("RANDOM()") }
|
||||||
|
|
||||||
|
|||||||
55
app/models/budget/result.rb
Normal file
55
app/models/budget/result.rb
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
class Budget
|
||||||
|
class Result
|
||||||
|
|
||||||
|
attr_accessor :budget, :heading, :money_spent, :current_investment
|
||||||
|
|
||||||
|
def initialize(budget, heading)
|
||||||
|
@budget = budget
|
||||||
|
@heading = heading
|
||||||
|
end
|
||||||
|
|
||||||
|
def calculate_winners
|
||||||
|
reset_winners
|
||||||
|
investments.each do |investment|
|
||||||
|
@current_investment = investment
|
||||||
|
if inside_budget?
|
||||||
|
set_winner
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def investments
|
||||||
|
heading.investments.selected.sort_by_ballots
|
||||||
|
end
|
||||||
|
|
||||||
|
def inside_budget?
|
||||||
|
available_budget >= @current_investment.price
|
||||||
|
end
|
||||||
|
|
||||||
|
def available_budget
|
||||||
|
total_budget - money_spent
|
||||||
|
end
|
||||||
|
|
||||||
|
def total_budget
|
||||||
|
heading.price
|
||||||
|
end
|
||||||
|
|
||||||
|
def money_spent
|
||||||
|
@money_spent ||= 0
|
||||||
|
end
|
||||||
|
|
||||||
|
def reset_winners
|
||||||
|
investments.update_all(winner: false)
|
||||||
|
end
|
||||||
|
|
||||||
|
def set_winner
|
||||||
|
@money_spent += @current_investment.price
|
||||||
|
@current_investment.update(winner: true)
|
||||||
|
end
|
||||||
|
|
||||||
|
def winners
|
||||||
|
investments.where(winner: true)
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
end
|
||||||
72
app/views/budgets/results/_results_table.html.erb
Normal file
72
app/views/budgets/results/_results_table.html.erb
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
<div class="small-12 medium-9 column" id="results-container">
|
||||||
|
<h3 class="inline-block">
|
||||||
|
<%= heading.name %>
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<%= link_to t("budgets.results.show_all_link"), "#",
|
||||||
|
class: "js-toggle-link button hollow margin-bottom float-right",
|
||||||
|
data: {'toggle-selector' => '.js-discarded',
|
||||||
|
'toggle-text' => t("budgets.results.hide_discarded_link")} %>
|
||||||
|
|
||||||
|
<table id="budget-investments-results" class="table-for-mobile">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th scope="col">
|
||||||
|
<%= t("budgets.results.spending_proposal") %>
|
||||||
|
</th>
|
||||||
|
<th scope="col" class="text-center">
|
||||||
|
<%= t("budgets.results.ballot_lines_count") %>
|
||||||
|
</th>
|
||||||
|
<th scope="col" class="text-center">
|
||||||
|
<%= t("budgets.results.price") %>
|
||||||
|
</th>
|
||||||
|
<th scope="col" class="text-right">
|
||||||
|
<%= format_price(heading.price) %><br>
|
||||||
|
<small><%= t("budgets.results.amount_available") %></small>
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
|
||||||
|
<tbody>
|
||||||
|
<% amount_available = heading.price %>
|
||||||
|
<% @result.investments.each do |investment| %>
|
||||||
|
<% if investment.winner? %>
|
||||||
|
<tr id="<%= dom_id(investment) %>"
|
||||||
|
class="spending_proposal success">
|
||||||
|
<% else %>
|
||||||
|
<tr id="<%= dom_id(investment) %>"
|
||||||
|
class="spending_proposal js-discarded" style="display:none">
|
||||||
|
<% end %>
|
||||||
|
<td>
|
||||||
|
<% if investment.winner? %>
|
||||||
|
<span class="icon-check">
|
||||||
|
<span class="sr-only">
|
||||||
|
<%= t("budgets.results.accepted") %>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
<% else %>
|
||||||
|
<span class="icon-x delete">
|
||||||
|
<span class="sr-only">
|
||||||
|
<%= t("budgets.results.discarded") %>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
<% end %>
|
||||||
|
<%= link_to investment.title,
|
||||||
|
budget_investment_path(@budget, investment) %>
|
||||||
|
</td>
|
||||||
|
<td class="text-center">
|
||||||
|
<%= investment.ballot_lines_count %>
|
||||||
|
</td>
|
||||||
|
<td class="text-center">
|
||||||
|
<%= format_price investment.price %>
|
||||||
|
</td>
|
||||||
|
<td class="small text-right"
|
||||||
|
title="<%= format_price(amount_available) %> - <%= format_price(investment.price) %>">
|
||||||
|
<%= format_price amount_available - investment.price %>
|
||||||
|
<% amount_available -= investment.price if investment.winner? %>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<% end %>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
38
app/views/budgets/results/show.html.erb
Normal file
38
app/views/budgets/results/show.html.erb
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
<% provide :title, t("budgets.results.page_title", budget: @budget.name) %>
|
||||||
|
<% content_for :canonical do %>
|
||||||
|
<%= render "shared/canonical", href: budget_results_url(@budget, heading_id: @result.heading) %>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
<div class="expanded budget no-margin-top">
|
||||||
|
<div class="row">
|
||||||
|
<div class="small-12 column padding text-center">
|
||||||
|
<%= back_link_to budget_path(@budget) %>
|
||||||
|
|
||||||
|
<h2 class="title">
|
||||||
|
<%= @budget.name %><br>
|
||||||
|
<%= t("budgets.results.heading") %>
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row margin-top">
|
||||||
|
<div class="small-12 medium-3 column">
|
||||||
|
<ul class="menu vertical no-margin-top no-padding-top">
|
||||||
|
<li>
|
||||||
|
<strong>
|
||||||
|
<%= t("budgets.results.geozone_selection_title") %>
|
||||||
|
</strong>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<% @budget.headings.each do |heading| %>
|
||||||
|
<li>
|
||||||
|
<%= link_to heading.name,
|
||||||
|
budget_results_path(@budget, heading_id: heading.id) %>
|
||||||
|
</li>
|
||||||
|
<% end %>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<%= render 'results_table', heading: @result.heading %>
|
||||||
|
</div>
|
||||||
@@ -35,6 +35,12 @@
|
|||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
|
<% if @budget.finished? %>
|
||||||
|
<%= link_to t("budgets.show.see_results"),
|
||||||
|
budget_results_path(@budget, heading_id: @budget.headings.first),
|
||||||
|
class: "button margin-top expanded" %>
|
||||||
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -115,3 +115,16 @@ en:
|
|||||||
unfeasible: See unfeasible investments
|
unfeasible: See unfeasible investments
|
||||||
unselected_title: Investments not selected for balloting phase
|
unselected_title: Investments not selected for balloting phase
|
||||||
unselected: See investments not selected for balloting phase
|
unselected: See investments not selected for balloting phase
|
||||||
|
see_results: See results
|
||||||
|
results:
|
||||||
|
page_title: "%{budget} - Results"
|
||||||
|
heading: "Participatory budget results"
|
||||||
|
geozone_selection_title: "By district"
|
||||||
|
spending_proposal: Proposal title
|
||||||
|
ballot_lines_count: Times selected
|
||||||
|
hide_discarded_link: Hide discarded
|
||||||
|
show_all_link: Show all
|
||||||
|
price: Cost
|
||||||
|
amount_available: Available budget
|
||||||
|
accepted: "Accepted spending proposal: "
|
||||||
|
discarded: "Discarded spending proposal: "
|
||||||
@@ -115,3 +115,16 @@ es:
|
|||||||
unfeasible: Ver las propuestas inviables
|
unfeasible: Ver las propuestas inviables
|
||||||
unselected_title: Propuestas no seleccionadas para la votación final
|
unselected_title: Propuestas no seleccionadas para la votación final
|
||||||
unselected: Ver las propuestas no seleccionadas para la votación final
|
unselected: Ver las propuestas no seleccionadas para la votación final
|
||||||
|
see_results: Ver resultados
|
||||||
|
results:
|
||||||
|
page_title: "%{budget} - Resultados"
|
||||||
|
heading: "Resultados presupuestos participativos"
|
||||||
|
geozone_selection_title: "Ámbito de actuación"
|
||||||
|
spending_proposal: Título
|
||||||
|
ballot_lines_count: Votos
|
||||||
|
hide_discarded_link: Ocultar descartadas
|
||||||
|
show_all_link: Mostrar todas
|
||||||
|
price: Coste
|
||||||
|
amount_available: Presupuesto disponible
|
||||||
|
accepted: "Propuesta de inversión aceptada: "
|
||||||
|
discarded: "Propuesta de inversión descartada: "
|
||||||
|
|||||||
@@ -77,6 +77,7 @@ Rails.application.routes.draw do
|
|||||||
resource :ballot, only: :show, controller: "budgets/ballots" do
|
resource :ballot, only: :show, controller: "budgets/ballots" do
|
||||||
resources :lines, controller: "budgets/ballot/lines", only: [:create, :destroy]
|
resources :lines, controller: "budgets/ballot/lines", only: [:create, :destroy]
|
||||||
end
|
end
|
||||||
|
resource :results, only: :show, controller: "budgets/results"
|
||||||
end
|
end
|
||||||
|
|
||||||
scope '/participatory_budget' do
|
scope '/participatory_budget' do
|
||||||
|
|||||||
@@ -422,6 +422,31 @@ Budget.balloting.last.investments.each do |investment|
|
|||||||
investment.update(selected: true, feasibility: "feasible")
|
investment.update(selected: true, feasibility: "feasible")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
puts " ✅"
|
||||||
|
print "Winner Investments"
|
||||||
|
|
||||||
|
budget = Budget.where(phase: "finished").last
|
||||||
|
(1..100).each do |i|
|
||||||
|
heading = budget.headings.reorder("RANDOM()").first
|
||||||
|
investment = Budget::Investment.create!(
|
||||||
|
author: User.reorder("RANDOM()").first,
|
||||||
|
heading: heading,
|
||||||
|
group: heading.group,
|
||||||
|
budget: heading.group.budget,
|
||||||
|
title: Faker::Lorem.sentence(3).truncate(60),
|
||||||
|
external_url: Faker::Internet.url,
|
||||||
|
description: "<p>#{Faker::Lorem.paragraphs.join('</p><p>')}</p>",
|
||||||
|
created_at: rand((Time.now - 1.week) .. Time.now),
|
||||||
|
feasibility: "feasible",
|
||||||
|
valuation_finished: true,
|
||||||
|
selected: true,
|
||||||
|
price: rand(10000 .. heading.price),
|
||||||
|
terms_of_service: "1")
|
||||||
|
end
|
||||||
|
budget.headings.each do |heading|
|
||||||
|
Budget::Result.new(budget, heading).calculate_winners
|
||||||
|
end
|
||||||
|
|
||||||
puts " ✅"
|
puts " ✅"
|
||||||
print "Creating Valuation Assignments"
|
print "Creating Valuation Assignments"
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
class AddWinnerToBudgetInvestments < ActiveRecord::Migration
|
||||||
|
def change
|
||||||
|
add_column :budget_investments, :winner, :boolean, default: false
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -11,7 +11,7 @@
|
|||||||
#
|
#
|
||||||
# It's strongly recommended that you check this file into your version control system.
|
# It's strongly recommended that you check this file into your version control system.
|
||||||
|
|
||||||
ActiveRecord::Schema.define(version: 20170517123042) do
|
ActiveRecord::Schema.define(version: 20170519084239) do
|
||||||
|
|
||||||
# These are extensions that must be enabled in order to support this database
|
# These are extensions that must be enabled in order to support this database
|
||||||
enable_extension "plpgsql"
|
enable_extension "plpgsql"
|
||||||
@@ -147,6 +147,7 @@ ActiveRecord::Schema.define(version: 20170517123042) do
|
|||||||
t.datetime "unfeasible_email_sent_at"
|
t.datetime "unfeasible_email_sent_at"
|
||||||
t.integer "ballot_lines_count", default: 0
|
t.integer "ballot_lines_count", default: 0
|
||||||
t.integer "previous_heading_id"
|
t.integer "previous_heading_id"
|
||||||
|
t.boolean "winner", default: false
|
||||||
end
|
end
|
||||||
|
|
||||||
add_index "budget_investments", ["administrator_id"], name: "index_budget_investments_on_administrator_id", using: :btree
|
add_index "budget_investments", ["administrator_id"], name: "index_budget_investments_on_administrator_id", using: :btree
|
||||||
|
|||||||
48
spec/features/budgets/results_spec.rb
Normal file
48
spec/features/budgets/results_spec.rb
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
feature 'Results' do
|
||||||
|
|
||||||
|
let(:budget) { create(:budget, phase: "finished") }
|
||||||
|
let(:group) { create(:budget_group, budget: budget) }
|
||||||
|
let(:heading) { create(:budget_heading, group: group, price: 1000) }
|
||||||
|
|
||||||
|
let!(:investment1) { create(:budget_investment, :selected, heading: heading, price: 200, ballot_lines_count: 900) }
|
||||||
|
let!(:investment2) { create(:budget_investment, :selected, heading: heading, price: 300, ballot_lines_count: 800) }
|
||||||
|
let!(:investment3) { create(:budget_investment, :selected, heading: heading, price: 500, ballot_lines_count: 700) }
|
||||||
|
let!(:investment4) { create(:budget_investment, :selected, heading: heading, price: 100, ballot_lines_count: 600) }
|
||||||
|
|
||||||
|
let!(:results) { Budget::Result.new(budget, heading).calculate_winners }
|
||||||
|
|
||||||
|
scenario "Diplays winner investments" do
|
||||||
|
visit budget_path(budget)
|
||||||
|
click_link "See results"
|
||||||
|
|
||||||
|
within("#budget-investments-results") do
|
||||||
|
expect(page).to have_content investment1.title
|
||||||
|
expect(page).to have_content investment2.title
|
||||||
|
expect(page).to have_content investment3.title
|
||||||
|
expect(page).to_not have_content investment4.title
|
||||||
|
|
||||||
|
expect(investment1.title).to appear_before(investment2.title)
|
||||||
|
expect(investment2.title).to appear_before(investment3.title)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
scenario "Displays non winner investments", :js do
|
||||||
|
visit budget_path(budget)
|
||||||
|
click_link "See results"
|
||||||
|
click_link "Show all"
|
||||||
|
|
||||||
|
within("#budget-investments-results") do
|
||||||
|
expect(page).to have_content investment1.title
|
||||||
|
expect(page).to have_content investment2.title
|
||||||
|
expect(page).to have_content investment3.title
|
||||||
|
expect(page).to have_content investment4.title
|
||||||
|
|
||||||
|
expect(investment1.title).to appear_before(investment2.title)
|
||||||
|
expect(investment2.title).to appear_before(investment3.title)
|
||||||
|
expect(investment3.title).to appear_before(investment4.title)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
35
spec/models/budget/result_spec.rb
Normal file
35
spec/models/budget/result_spec.rb
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
describe Budget::Result do
|
||||||
|
|
||||||
|
describe "calculate_winners" do
|
||||||
|
let(:budget) { create(:budget) }
|
||||||
|
let(:group) { create(:budget_group, budget: budget) }
|
||||||
|
let(:heading) { create(:budget_heading, group: group, price: 1000) }
|
||||||
|
|
||||||
|
it "calculates a budget's winner investments" do
|
||||||
|
investment1 = create(:budget_investment, :selected, heading: heading, price: 200, ballot_lines_count: 900)
|
||||||
|
investment2 = create(:budget_investment, :selected, heading: heading, price: 300, ballot_lines_count: 800)
|
||||||
|
investment3 = create(:budget_investment, :selected, heading: heading, price: 500, ballot_lines_count: 700)
|
||||||
|
investment4 = create(:budget_investment, :selected, heading: heading, price: 100, ballot_lines_count: 600)
|
||||||
|
|
||||||
|
result = Budget::Result.new(budget, heading)
|
||||||
|
result.calculate_winners
|
||||||
|
|
||||||
|
expect(result.winners).to eq([investment1, investment2, investment3])
|
||||||
|
end
|
||||||
|
|
||||||
|
it "resets winners before recalculating" do
|
||||||
|
investment1 = create(:budget_investment, :selected, heading: heading, price: 200, ballot_lines_count: 900, winner: true)
|
||||||
|
investment2 = create(:budget_investment, :selected, heading: heading, price: 300, ballot_lines_count: 800, winner: true)
|
||||||
|
investment3 = create(:budget_investment, :selected, heading: heading, price: 500, ballot_lines_count: 700, winner: true)
|
||||||
|
investment4 = create(:budget_investment, :selected, heading: heading, price: 100, ballot_lines_count: 600, winner: true)
|
||||||
|
|
||||||
|
result = Budget::Result.new(budget, heading)
|
||||||
|
result.calculate_winners
|
||||||
|
|
||||||
|
expect(result.winners).to eq([investment1, investment2, investment3])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
Reference in New Issue
Block a user