diff --git a/app/assets/stylesheets/admin.scss b/app/assets/stylesheets/admin.scss index e864d9781..fd6ed0815 100644 --- a/app/assets/stylesheets/admin.scss +++ b/app/assets/stylesheets/admin.scss @@ -312,6 +312,11 @@ body.admin { } } +[class^="icon-"].delete { + border: 0; + font-size: $base-font-size; +} + .verified { color: $check; diff --git a/app/assets/stylesheets/layout.scss b/app/assets/stylesheets/layout.scss index 64684505c..fa0fd3455 100644 --- a/app/assets/stylesheets/layout.scss +++ b/app/assets/stylesheets/layout.scss @@ -234,6 +234,14 @@ a { color: $brand; } } + + &.no-margin-top { + margin-top: 0; + } + + &.no-padding-top { + padding-top: 0; + } } .small { @@ -1194,6 +1202,16 @@ table { } } +.table-for-mobile { + + @include breakpoint(medium down) { + th, td { + display: block; + text-align: left; + } + } +} + // 12. Social // ---------- diff --git a/app/controllers/budgets/results_controller.rb b/app/controllers/budgets/results_controller.rb new file mode 100644 index 000000000..aa94eb68b --- /dev/null +++ b/app/controllers/budgets/results_controller.rb @@ -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 \ No newline at end of file diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 0c23c0d22..6a040ec69 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -55,4 +55,8 @@ module ApplicationHelper def content_block(name, locale) SiteCustomization::ContentBlock.block_for(name, locale) end + + def format_price(number) + number_to_currency(number, precision: 0, locale: I18n.default_locale) + end end diff --git a/app/models/budget/investment.rb b/app/models/budget/investment.rb index 7d0aafb8a..56a50deec 100644 --- a/app/models/budget/investment.rb +++ b/app/models/budget/investment.rb @@ -33,6 +33,7 @@ class Budget validates :terms_of_service, acceptance: { allow_nil: false }, on: :create 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_random, -> { reorder("RANDOM()") } diff --git a/app/models/budget/result.rb b/app/models/budget/result.rb new file mode 100644 index 000000000..f29bc72cc --- /dev/null +++ b/app/models/budget/result.rb @@ -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 \ No newline at end of file diff --git a/app/views/budgets/results/_results_table.html.erb b/app/views/budgets/results/_results_table.html.erb new file mode 100644 index 000000000..9024899b6 --- /dev/null +++ b/app/views/budgets/results/_results_table.html.erb @@ -0,0 +1,72 @@ +
+

+ <%= heading.name %> +

+ + <%= 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")} %> + + + + + + + + + + + + + <% amount_available = heading.price %> + <% @result.investments.each do |investment| %> + <% if investment.winner? %> + + <% else %> + + <% end %> + + + + + + <% end %> + +
+ <%= t("budgets.results.spending_proposal") %> + + <%= t("budgets.results.ballot_lines_count") %> + + <%= t("budgets.results.price") %> + + <%= format_price(heading.price) %>
+ <%= t("budgets.results.amount_available") %> +
+
diff --git a/app/views/budgets/results/show.html.erb b/app/views/budgets/results/show.html.erb new file mode 100644 index 000000000..d9253b785 --- /dev/null +++ b/app/views/budgets/results/show.html.erb @@ -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 %> + +
+
+
+ <%= back_link_to budget_path(@budget) %> + +

+ <%= @budget.name %>
+ <%= t("budgets.results.heading") %> +

+
+
+
+ +
+
+ +
+ + <%= render 'results_table', heading: @result.heading %> +
diff --git a/app/views/budgets/show.html.erb b/app/views/budgets/show.html.erb index 6cd74849d..814488d9d 100644 --- a/app/views/budgets/show.html.erb +++ b/app/views/budgets/show.html.erb @@ -35,6 +35,12 @@ <% 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 %> diff --git a/config/locales/budgets.en.yml b/config/locales/budgets.en.yml index e7ee15b5a..570f9da35 100644 --- a/config/locales/budgets.en.yml +++ b/config/locales/budgets.en.yml @@ -114,4 +114,17 @@ en: unfeasible_title: Unfeasible investments unfeasible: See unfeasible investments unselected_title: Investments not selected for balloting phase - unselected: See investments not selected for balloting phase \ No newline at end of file + 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: " \ No newline at end of file diff --git a/config/locales/budgets.es.yml b/config/locales/budgets.es.yml index 7db4f73e6..a8e7fa5fa 100644 --- a/config/locales/budgets.es.yml +++ b/config/locales/budgets.es.yml @@ -114,4 +114,17 @@ es: unfeasible_title: Propuestas inviables unfeasible: Ver las propuestas inviables unselected_title: Propuestas no seleccionadas para la votación final - unselected: Ver las propuestas no seleccionadas para la votación final \ No newline at end of file + 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: " diff --git a/config/routes.rb b/config/routes.rb index 5fd94eaaf..4007595db 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -77,6 +77,7 @@ Rails.application.routes.draw do resource :ballot, only: :show, controller: "budgets/ballots" do resources :lines, controller: "budgets/ballot/lines", only: [:create, :destroy] end + resource :results, only: :show, controller: "budgets/results" end scope '/participatory_budget' do diff --git a/db/dev_seeds.rb b/db/dev_seeds.rb index 2082102a1..9898ad46f 100644 --- a/db/dev_seeds.rb +++ b/db/dev_seeds.rb @@ -422,6 +422,31 @@ Budget.balloting.last.investments.each do |investment| investment.update(selected: true, feasibility: "feasible") 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: "

#{Faker::Lorem.paragraphs.join('

')}

", + 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 " ✅" print "Creating Valuation Assignments" diff --git a/db/migrate/20170519084239_add_winner_to_budget_investments.rb b/db/migrate/20170519084239_add_winner_to_budget_investments.rb new file mode 100644 index 000000000..af6085468 --- /dev/null +++ b/db/migrate/20170519084239_add_winner_to_budget_investments.rb @@ -0,0 +1,5 @@ +class AddWinnerToBudgetInvestments < ActiveRecord::Migration + def change + add_column :budget_investments, :winner, :boolean, default: false + end +end diff --git a/db/schema.rb b/db/schema.rb index d3953f638..8a34df55d 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # 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 enable_extension "plpgsql" @@ -147,6 +147,7 @@ ActiveRecord::Schema.define(version: 20170517123042) do t.datetime "unfeasible_email_sent_at" t.integer "ballot_lines_count", default: 0 t.integer "previous_heading_id" + t.boolean "winner", default: false end add_index "budget_investments", ["administrator_id"], name: "index_budget_investments_on_administrator_id", using: :btree diff --git a/spec/features/budgets/results_spec.rb b/spec/features/budgets/results_spec.rb new file mode 100644 index 000000000..961dd287e --- /dev/null +++ b/spec/features/budgets/results_spec.rb @@ -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 \ No newline at end of file diff --git a/spec/models/budget/result_spec.rb b/spec/models/budget/result_spec.rb new file mode 100644 index 000000000..f96428221 --- /dev/null +++ b/spec/models/budget/result_spec.rb @@ -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 \ No newline at end of file