diff --git a/app/assets/stylesheets/admin.scss b/app/assets/stylesheets/admin.scss index 196d0be17..7eab8c070 100644 --- a/app/assets/stylesheets/admin.scss +++ b/app/assets/stylesheets/admin.scss @@ -384,7 +384,7 @@ body.admin { } } -.admin-content .select-geozone { +.admin-content .select-geozone, .admin-content .select-heading { a { display: block; diff --git a/app/controllers/admin/budget_groups_controller.rb b/app/controllers/admin/budget_groups_controller.rb index 18f5a6b12..9c5a54b98 100644 --- a/app/controllers/admin/budget_groups_controller.rb +++ b/app/controllers/admin/budget_groups_controller.rb @@ -1,4 +1,6 @@ class Admin::BudgetGroupsController < Admin::BaseController + include FeatureFlags + feature_flag :budgets def create @budget = Budget.find params[:budget_id] diff --git a/app/controllers/valuation/budget_investments_controller.rb b/app/controllers/valuation/budget_investments_controller.rb new file mode 100644 index 000000000..348f275da --- /dev/null +++ b/app/controllers/valuation/budget_investments_controller.rb @@ -0,0 +1,81 @@ +class Valuation::BudgetInvestmentsController < Valuation::BaseController + include FeatureFlags + feature_flag :budgets + + before_action :restrict_access_to_assigned_items, only: [:show, :edit, :valuate] + before_action :load_budget + + has_filters %w{valuating valuation_finished}, only: :index + + load_and_authorize_resource :investment, class: "Budget::Investment" + + def index + @heading_filters = heading_filters + if current_user.valuator? && @budget.present? + @investments = @budget.investments.scoped_filter(params_for_current_valuator, @current_filter).order(cached_votes_up: :desc).page(params[:page]) + else + @investments = Budget::Investment.none.page(params[:page]) + end + end + + def valuate + if valid_price_params? && @investment.update(valuation_params) + + if @investment.unfeasible_email_pending? + @investment.send_unfeasible_email + end + + redirect_to valuation_budget_investment_path(@investment), notice: t('valuation.budget_investments.notice.valuate') + else + render action: :edit + end + end + + private + + def load_budget + @budget = Budget.find(params[:budget_id]) + end + + def heading_filters + investments = @budget.investments.by_valuator(current_user.valuator.try(:id)).valuation_open.select(:heading_id).all.to_a + + [ { name: t('valuation.budget_investments.index.headings_filter_all'), + id: nil, + pending_count: investments.size + } + ] + Budget::Heading.where(id: investments.map(&:heading_id).uniq).order(name: :asc).collect do |h| + { name: h.name, + id: h.id, + pending_count: investments.count{|x| x.heading_id == h.id} + } + end + end + + def params_for_current_valuator + Budget::Investment.filter_params(params).merge({valuator_id: current_user.valuator.id, budget_id: @budget.id}) + end + + def valuation_params + params[:budget_investment][:feasible] = nil if params[:budget_investment][:feasible] == 'nil' + + params.require(:budget_investment).permit(:price, :price_first_year, :price_explanation, :feasible, :feasible_explanation, :duration, :valuation_finished, :internal_comments) + end + + def restrict_access_to_assigned_items + raise ActionController::RoutingError.new('Not Found') unless current_user.administrator? || ValuatorAssignment.exists?(investment_id: params[:id], valuator_id: current_user.valuator.id) + end + + def valid_price_params? + if /\D/.match params[:budget_investment][:price] + @investment.errors.add(:price, I18n.t('budget.investments.wrong_price_format')) + end + + if /\D/.match params[:budget_investment][:price_first_year] + @investment.errors.add(:price_first_year, I18n.t('budget.investments.wrong_price_format')) + end + + @investment.errors.empty? + end + +end \ No newline at end of file diff --git a/app/controllers/valuation/budgets_controller.rb b/app/controllers/valuation/budgets_controller.rb new file mode 100644 index 000000000..e96d018dd --- /dev/null +++ b/app/controllers/valuation/budgets_controller.rb @@ -0,0 +1,13 @@ +class Valuation::BudgetsController < Valuation::BaseController + include FeatureFlags + feature_flag :budgets + + has_filters %w{open finished}, only: :index + + load_and_authorize_resource + + def index + @budgets = Budget.send(@current_filter).order(created_at: :desc).page(params[:page]) + end + +end \ No newline at end of file diff --git a/app/controllers/valuation/spending_proposals_controller.rb b/app/controllers/valuation/spending_proposals_controller.rb index d3bc585b1..f130baec4 100644 --- a/app/controllers/valuation/spending_proposals_controller.rb +++ b/app/controllers/valuation/spending_proposals_controller.rb @@ -58,7 +58,7 @@ class Valuation::SpendingProposalsController < Valuation::BaseController end def params_for_current_valuator - params.merge({valuator_id: current_user.valuator.id}) + params.merge({valuator_id: current_user.valuator.id}) end def restrict_access_to_assigned_items diff --git a/app/helpers/valuation_helper.rb b/app/helpers/valuation_helper.rb index 0983ea567..ded5fa0ed 100644 --- a/app/helpers/valuation_helper.rb +++ b/app/helpers/valuation_helper.rb @@ -11,14 +11,14 @@ module ValuationHelper def assigned_valuators_info(valuators) case valuators.size when 0 - t("valuation.spending_proposals.index.no_valuators_assigned") + t("valuation.budget_investments.index.no_valuators_assigned") when 1 - "".html_safe + + "".html_safe + valuators.first.name + "".html_safe else "".html_safe + - t('valuation.spending_proposals.index.valuators_assigned', count: valuators.size) + + t('valuation.budget_investments.index.valuators_assigned', count: valuators.size) + "".html_safe end end diff --git a/app/models/abilities/valuator.rb b/app/models/abilities/valuator.rb index 85c598d12..d3979a640 100644 --- a/app/models/abilities/valuator.rb +++ b/app/models/abilities/valuator.rb @@ -5,7 +5,7 @@ module Abilities def initialize(user) valuator = user.valuator can [:read, :update, :valuate], SpendingProposal - can [:update, :valuate], Budget::Investment, id: valuator.investment_ids, budget: { valuating: true } + can [:read, :update, :valuate], Budget::Investment, id: valuator.investment_ids, budget: { valuating: true } end end end diff --git a/app/models/budget.rb b/app/models/budget.rb index b8fd34da9..e716c0181 100644 --- a/app/models/budget.rb +++ b/app/models/budget.rb @@ -13,10 +13,10 @@ class Budget < ActiveRecord::Base has_many :ballots, dependent: :destroy has_many :groups, dependent: :destroy has_many :headings, through: :groups - has_many :investments, through: :headings - scope :open, -> { where.not(phase: "finished") } - scope :finished, -> { where(phase: "finished") } + scope :open, -> { where.not(phase: "finished") } + scope :finished, -> { where(phase: "finished") } + scope :valuating, -> { where(valuating: true) } def on_hold? phase == "on_hold" diff --git a/app/views/valuation/_menu.html.erb b/app/views/valuation/_menu.html.erb index c63b6421a..3908b9279 100644 --- a/app/views/valuation/_menu.html.erb +++ b/app/views/valuation/_menu.html.erb @@ -13,5 +13,14 @@ <% end %> + <% if feature?(:budgets) %> +
  • > + <%= link_to valuation_budgets_path do %> + + <%= t("valuation.menu.budgets") %> + <% end %> +
  • + <% end %> + diff --git a/app/views/valuation/budget_investments/index.html.erb b/app/views/valuation/budget_investments/index.html.erb new file mode 100644 index 000000000..9b82f7f5a --- /dev/null +++ b/app/views/valuation/budget_investments/index.html.erb @@ -0,0 +1,42 @@ +

    <%= @budget.name %> - <%= t("valuation.budget_investments.index.title") %>

    + +
    + <% @heading_filters.each_slice(8) do |slice| %> +
    + <% slice.each do |filter| %> + <%= link_to valuation_budget_budget_investments_path(budget_id: @budget.id, heading_id: filter[:id]), + class: "#{'active' if params[:heading_id].to_s == filter[:id].to_s}" do %> + <%= filter[:name] %> (<%= filter[:pending_count] %>) + <% end %> + <% end %> +
    + <% end %> +
    + +<%= render 'shared/filter_subnav', i18n_namespace: "valuation.budget_investments.index" %> + +

    <%= page_entries_info @investments %>

    + + + <% @investments.each do |investment| %> + + + + + + + + <% end %> +
    + <%= investment.id %> + + <%= link_to investment.title, valuation_budget_budget_investment_path(@budget, investment) %> + + <%= link_to t("valuation.budget_investments.index.edit"), edit_valuation_budget_budget_investment_path(@budget, investment) %> + + <%= assigned_valuators_info(investment.valuators) %> + + <%= investment.heading.name %> +
    + +<%= paginate @investments %> diff --git a/app/views/valuation/budgets/index.html.erb b/app/views/valuation/budgets/index.html.erb new file mode 100644 index 000000000..014633373 --- /dev/null +++ b/app/views/valuation/budgets/index.html.erb @@ -0,0 +1,17 @@ +

    <%= t("valuation.budgets.index.title") %>

    + +<%= render 'shared/filter_subnav', i18n_namespace: "valuation.budgets.index" %> + +

    <%= page_entries_info @budgets %>

    + + + <% @budgets.each do |budget| %> + + + + <% end %> +
    + <%= link_to budget.name, valuation_budget_budget_investments_path(budget_id: budget.id) %> +
    + +<%= paginate @budgets %> \ No newline at end of file diff --git a/config/i18n-tasks.yml b/config/i18n-tasks.yml index 161ed6dfd..9b675ffd0 100644 --- a/config/i18n-tasks.yml +++ b/config/i18n-tasks.yml @@ -132,6 +132,8 @@ ignore_unused: - 'moderation.debates.index.filter*' - 'moderation.debates.index.order*' - 'valuation.spending_proposals.index.filter*' + - 'valuation.budgets.index.filter*' + - 'valuation.budget_investments.index.filter*' - 'users.show.filters.*' - 'debates.index.select_order' - 'debates.index.orders.*' diff --git a/config/locales/valuation.en.yml b/config/locales/valuation.en.yml index c3b6d8283..b1cf1ff81 100644 --- a/config/locales/valuation.en.yml +++ b/config/locales/valuation.en.yml @@ -3,8 +3,27 @@ en: valuation: menu: title: Valuation + budgets: Participatory budgets spending_proposals: Spending proposals + budgets: + index: + title: Participatory budgets + filters: + open: Open + finished: Finished budget_investments: + index: + headings_filter_all: All headings + filters: + valuation_open: Open + valuating: Under valuation + valuation_finished: Valuation finished + title: Investment projects + edit: Edit + valuators_assigned: + one: Assigned valuator + other: "%{count} valuators assigned" + no_valuators_assigned: No valuators assigned show: back: Back heading: Investment project @@ -27,6 +46,8 @@ en: responsibles: Responsibles assigned_admin: Assigned admin assigned_valuators: Assigned valuators + notice: + valuate: "Dossier updated" spending_proposals: index: geozone_filter_all: All zones @@ -36,10 +57,6 @@ en: valuation_finished: Valuation finished title: Investment projects for participatory budgeting edit: Edit - valuators_assigned: - one: Assigned valuator - other: "%{count} valuators assigned" - no_valuators_assigned: No valuators assigned show: back: Back heading: Investment project diff --git a/config/locales/valuation.es.yml b/config/locales/valuation.es.yml index 34771587d..163ee658d 100644 --- a/config/locales/valuation.es.yml +++ b/config/locales/valuation.es.yml @@ -3,8 +3,27 @@ es: valuation: menu: title: Evaluación + budgets: Presupuestos participativos spending_proposals: Propuestas de inversión + budgets: + index: + title: Presupuestos participativos + filters: + open: Abiertos + finished: Terminados budget_investments: + index: + headings_filter_all: Todas las partidas + filters: + valuation_open: Abiertas + valuating: En evaluación + valuation_finished: Evaluación finalizada + title: Propuestas de inversión + edit: Editar + valuators_assigned: + one: Evaluador asignado + other: "%{count} evaluadores asignados" + no_valuators_assigned: Sin evaluador show: back: Volver heading: Propuesta de inversión @@ -27,6 +46,8 @@ es: responsibles: Responsables assigned_admin: Administrador asignado assigned_valuators: Evaluadores asignados + notice: + valuate: "Dossier actualizado" spending_proposals: index: geozone_filter_all: Todos los ámbitos de actuación @@ -36,10 +57,6 @@ es: valuation_finished: Evaluación finalizada title: Propuestas de inversión para presupuestos participativos edit: Editar - valuators_assigned: - one: Evaluador asignado - other: "%{count} evaluadores asignados" - no_valuators_assigned: Sin evaluador show: back: Volver heading: Propuesta de inversión diff --git a/config/routes.rb b/config/routes.rb index b22806dbd..c10b4ba2b 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -237,11 +237,17 @@ Rails.application.routes.draw do end namespace :valuation do - root to: "spending_proposals#index" + root to: "budgets#index" resources :spending_proposals, only: [:index, :show, :edit] do patch :valuate, on: :member end + + resources :budgets, only: :index do + resources :budget_investments, only: [:index, :show, :edit] do + patch :valuate, on: :member + end + end end namespace :management do diff --git a/spec/features/admin/budgets_spec.rb b/spec/features/admin/budgets_spec.rb index a87cd741a..b01c798cc 100644 --- a/spec/features/admin/budgets_spec.rb +++ b/spec/features/admin/budgets_spec.rb @@ -9,7 +9,7 @@ feature 'Admin budgets' do context 'Feature flag' do - xscenario 'Disabled with a feature flag' do + scenario 'Disabled with a feature flag' do Setting['feature.budgets'] = nil expect{ visit admin_budgets_path }.to raise_exception(FeatureFlags::FeatureDisabled) end diff --git a/spec/features/valuation/budget_investments_spec.rb b/spec/features/valuation/budget_investments_spec.rb new file mode 100644 index 000000000..53f60e416 --- /dev/null +++ b/spec/features/valuation/budget_investments_spec.rb @@ -0,0 +1,161 @@ +require 'rails_helper' + +feature 'Valuation budget investments' do + + background do + @valuator = create(:valuator, user: create(:user, username: 'Rachel', email: 'rachel@valuators.org')) + login_as(@valuator.user) + @budget = create(:budget, valuating: true) + end + + scenario 'Disabled with a feature flag' do + Setting['feature.budgets'] = nil + expect{ visit valuation_budget_budget_investments_path(create(:budget)) }.to raise_exception(FeatureFlags::FeatureDisabled) + end + + scenario 'Index shows budget investments assigned to current valuator' do + investment1 = create(:budget_investment, budget: @budget) + investment2 = create(:budget_investment, budget: @budget) + + investment1.valuators << @valuator + + visit valuation_budget_budget_investments_path(@budget) + + expect(page).to have_content(investment1.title) + expect(page).to_not have_content(investment2.title) + end + + scenario 'Index shows no budget investment to admins no valuators' do + investment1 = create(:budget_investment, budget: @budget) + investment2 = create(:budget_investment, budget: @budget) + + investment1.valuators << @valuator + + logout + login_as create(:administrator).user + visit valuation_budget_budget_investments_path(@budget) + + expect(page).to_not have_content(investment1.title) + expect(page).to_not have_content(investment2.title) + end + + scenario 'Index orders budget investments by votes' do + investment10 = create(:budget_investment, budget: @budget, cached_votes_up: 10) + investment100 = create(:budget_investment, budget: @budget, cached_votes_up: 100) + investment1 = create(:budget_investment, budget: @budget, cached_votes_up: 1) + + investment1.valuators << @valuator + investment10.valuators << @valuator + investment100.valuators << @valuator + + visit valuation_budget_budget_investments_path(@budget) + + expect(investment100.title).to appear_before(investment10.title) + expect(investment10.title).to appear_before(investment1.title) + end + + scenario 'Index shows assignments info' do + investment1 = create(:budget_investment, budget: @budget) + investment2 = create(:budget_investment, budget: @budget) + investment3 = create(:budget_investment, budget: @budget) + + valuator1 = create(:valuator, user: create(:user)) + valuator2 = create(:valuator, user: create(:user)) + valuator3 = create(:valuator, user: create(:user)) + + investment1.valuator_ids = [@valuator.id] + investment2.valuator_ids = [@valuator.id, valuator1.id, valuator2.id] + investment3.valuator_ids = [@valuator.id, valuator3.id] + + visit valuation_budget_budget_investments_path(@budget) + + within("#budget_investment_#{investment1.id}") do + expect(page).to have_content("Rachel") + end + + within("#budget_investment_#{investment2.id}") do + expect(page).to have_content("3 valuators assigned") + end + + within("#budget_investment_#{investment3.id}") do + expect(page).to have_content("2 valuators assigned") + end + end + + scenario "Index filtering by heading", :js do + group = create(:budget_group, budget: @budget) + heading1 = create(:budget_heading, name: "District 9", group: group) + heading2 = create(:budget_heading, name: "Down to the river", group: group) + investment1 = create(:budget_investment, title: "Realocate visitors", heading: heading1, group: group, budget: @budget) + investment2 = create(:budget_investment, title: "Destroy the city", heading: heading2, group: group, budget: @budget) + investment1.valuators << @valuator + investment2.valuators << @valuator + + visit valuation_budget_budget_investments_path(@budget) + + expect(page).to have_link("Realocate visitors") + expect(page).to have_link("Destroy the city") + + + expect(page).to have_content "All headings (2)" + expect(page).to have_content "District 9 (1)" + expect(page).to have_content "Down to the river (1)" + + click_link "District 9", exact: false + + expect(page).to have_link("Realocate visitors") + expect(page).to_not have_link("Destroy the city") + + click_link "Down to the river", exact: false + + expect(page).to have_link("Destroy the city") + expect(page).to_not have_link("Realocate visitors") + + click_link "All headings", exact: false + expect(page).to have_link("Realocate visitors") + expect(page).to have_link("Destroy the city") + end + + scenario "Current filter is properly highlighted" do + filters_links = {'valuating' => 'Under valuation', + 'valuation_finished' => 'Valuation finished'} + + visit valuation_budget_budget_investments_path(@budget) + + expect(page).to_not have_link(filters_links.values.first) + filters_links.keys.drop(1).each { |filter| expect(page).to have_link(filters_links[filter]) } + + filters_links.each_pair do |current_filter, link| + visit valuation_budget_budget_investments_path(@budget, filter: current_filter) + + expect(page).to_not have_link(link) + + (filters_links.keys - [current_filter]).each do |filter| + expect(page).to have_link(filters_links[filter]) + end + end + end + + scenario "Index filtering by valuation status" do + valuating = create(:budget_investment, budget: @budget, title: "Ongoing valuation") + valuated = create(:budget_investment, budget: @budget, title: "Old idea", valuation_finished: true) + valuating.valuators << @valuator + valuated.valuators << @valuator + + visit valuation_budget_budget_investments_path(@budget) + + expect(page).to have_content("Ongoing valuation") + expect(page).to_not have_content("Old idea") + + visit valuation_budget_budget_investments_path(@budget, filter: 'valuating') + + expect(page).to have_content("Ongoing valuation") + expect(page).to_not have_content("Old idea") + + visit valuation_budget_budget_investments_path(@budget, filter: 'valuation_finished') + + expect(page).to_not have_content("Ongoing valuation") + expect(page).to have_content("Old idea") + end + +end \ No newline at end of file diff --git a/spec/features/valuation/budgets_spec.rb b/spec/features/valuation/budgets_spec.rb new file mode 100644 index 000000000..0f61b8078 --- /dev/null +++ b/spec/features/valuation/budgets_spec.rb @@ -0,0 +1,74 @@ +require 'rails_helper' + +feature 'Valuation budgets' do + + background do + @valuator = create(:valuator, user: create(:user, username: 'Rachel', email: 'rachel@valuators.org')) + login_as(@valuator.user) + end + + scenario 'Disabled with a feature flag' do + Setting['feature.budgets'] = nil + expect{ visit valuation_budgets_path }.to raise_exception(FeatureFlags::FeatureDisabled) + end + + context 'Index' do + + scenario 'Displaying budgets' do + budget = create(:budget) + visit valuation_budgets_path + + expect(page).to have_content(budget.name) + end + + scenario 'Filters by phase' do + budget1 = create(:budget) + budget2 = create(:budget, :accepting) + budget3 = create(:budget, :selecting) + budget4 = create(:budget, :balloting) + budget5 = create(:budget, :finished) + + visit valuation_budgets_path + expect(page).to have_content(budget1.name) + expect(page).to have_content(budget2.name) + expect(page).to have_content(budget3.name) + expect(page).to have_content(budget4.name) + expect(page).to_not have_content(budget5.name) + + click_link 'Finished' + expect(page).to_not have_content(budget1.name) + expect(page).to_not have_content(budget2.name) + expect(page).to_not have_content(budget3.name) + expect(page).to_not have_content(budget4.name) + expect(page).to have_content(budget5.name) + + click_link 'Open' + expect(page).to have_content(budget1.name) + expect(page).to have_content(budget2.name) + expect(page).to have_content(budget3.name) + expect(page).to have_content(budget4.name) + expect(page).to_not have_content(budget5.name) + end + + scenario 'Current filter is properly highlighted' do + filters_links = {'open' => 'Open', 'finished' => 'Finished'} + + visit valuation_budgets_path + + expect(page).to_not have_link(filters_links.values.first) + filters_links.keys.drop(1).each { |filter| expect(page).to have_link(filters_links[filter]) } + + filters_links.each_pair do |current_filter, link| + visit valuation_budgets_path(filter: current_filter) + + expect(page).to_not have_link(link) + + (filters_links.keys - [current_filter]).each do |filter| + expect(page).to have_link(filters_links[filter]) + end + end + end + + end + +end \ No newline at end of file