Use separate actions to select/deselect investments

This is consistent to what we usually do. Also, we're applying the same
criteria mentioned in commit 72704d776:

> We're also making these actions idempotent, so sending many requests
> to the same action will get the same result, which wasn't the case
> with the `toggle` action. Although it's a low probability case, the
> `toggle` action could result in [selecting an investment] when trying
> to [deselect] it if someone else has [deselected it] it between the
> time the page loaded and the time the admin clicked on the
> "[Selected]" button.
This commit is contained in:
Javi Martín
2024-10-09 01:56:32 +02:00
parent 463112c2ea
commit 54a48d63e1
7 changed files with 87 additions and 25 deletions

View File

@@ -1,4 +1,4 @@
<% if can?(:toggle_selection, investment) %> <% if can?(action, investment) %>
<%= render Admin::ToggleSwitchComponent.new(action, investment, pressed: selected?, **options) %> <%= render Admin::ToggleSwitchComponent.new(action, investment, pressed: selected?, **options) %>
<% elsif selected? %> <% elsif selected? %>
<%= selected_text %> <%= selected_text %>

View File

@@ -14,20 +14,26 @@ class Admin::BudgetInvestments::ToggleSelectionComponent < ApplicationComponent
end end
def action def action
:toggle_selection if selected?
:deselect
else
:select
end
end end
def path def path
toggle_selection_admin_budget_budget_investment_path( url_for({
investment.budget, controller: "admin/budget_investments",
investment, action: action,
budget_id: investment.budget,
id: investment,
filter: params[:filter], filter: params[:filter],
sort_by: params[:sort_by], sort_by: params[:sort_by],
min_total_supports: params[:min_total_supports], min_total_supports: params[:min_total_supports],
max_total_supports: params[:max_total_supports], max_total_supports: params[:max_total_supports],
advanced_filters: params[:advanced_filters], advanced_filters: params[:advanced_filters],
page: params[:page] page: params[:page]
) })
end end
def options def options

View File

@@ -9,7 +9,7 @@ class Admin::BudgetInvestmentsController < Admin::BaseController
has_filters %w[all], only: :index has_filters %w[all], only: :index
before_action :load_budget before_action :load_budget
before_action :load_investment, only: [:show, :edit, :update, :toggle_selection] before_action :load_investment, except: [:index]
before_action :load_ballot, only: [:show, :index] before_action :load_ballot, only: [:show, :index]
before_action :parse_valuation_filters before_action :parse_valuation_filters
before_action :load_investments, only: :index before_action :load_investments, only: :index
@@ -60,10 +60,18 @@ class Admin::BudgetInvestmentsController < Admin::BaseController
end end
end end
def toggle_selection def select
authorize! :toggle_selection, @investment authorize! :select, @investment
@investment.toggle :selected @investment.update!(selected: true)
@investment.save!
render :toggle_selection
end
def deselect
authorize! :deselect, @investment
@investment.update!(selected: false)
render :toggle_selection
end end
private private

View File

@@ -75,7 +75,7 @@ module Abilities
can [:valuate, :comment_valuation], Budget::Investment can [:valuate, :comment_valuation], Budget::Investment
cannot [:admin_update, :valuate, :comment_valuation], cannot [:admin_update, :valuate, :comment_valuation],
Budget::Investment, budget: { phase: "finished" } Budget::Investment, budget: { phase: "finished" }
can :toggle_selection, Budget::Investment do |investment| can [:select, :deselect], Budget::Investment do |investment|
investment.feasible? && investment.valuation_finished? && !investment.budget.finished? investment.feasible? && investment.valuation_finished? && !investment.budget.finished?
end end

View File

@@ -66,7 +66,10 @@ namespace :admin do
end end
resources :budget_investments, only: [:index, :show, :edit, :update] do resources :budget_investments, only: [:index, :show, :edit, :update] do
member { patch :toggle_selection } member do
patch :select
patch :deselect
end
resources :audits, only: :show, controller: "budget_investment_audits" resources :audits, only: :show, controller: "budget_investment_audits"
resources :milestones, controller: "budget_investment_milestones" resources :milestones, controller: "budget_investment_milestones"

View File

@@ -38,18 +38,63 @@ describe Admin::BudgetInvestmentsController, :admin do
end end
end end
describe "PATCH toggle selection" do describe "PATCH select" do
it "uses the toggle_selection authorization rules" do let(:investment) { create(:budget_investment, :feasible, :finished) }
investment = create(:budget_investment)
patch :toggle_selection, xhr: true, params: { it "selects the investment" do
id: investment, expect do
budget_id: investment.budget, patch :select, xhr: true, params: { id: investment, budget_id: investment.budget }
} end.to change { investment.reload.selected? }.from(false).to(true)
expect(response).to be_successful
end
it "does not modify already selected investments" do
investment.update!(selected: true)
expect do
patch :select, xhr: true, params: { id: investment, budget_id: investment.budget }
end.not_to change { investment.reload.selected? }
end
it "uses the select/deselect authorization rules" do
investment.update!(valuation_finished: false)
patch :select, xhr: true, params: { id: investment, budget_id: investment.budget }
expect(flash[:alert]).to eq "You do not have permission to carry out the action " \ expect(flash[:alert]).to eq "You do not have permission to carry out the action " \
"'toggle_selection' on Investment." "'select' on Investment."
expect(investment).not_to be_selected expect(investment).not_to be_selected
end end
end end
describe "PATCH deselect" do
let(:investment) { create(:budget_investment, :feasible, :finished, :selected) }
it "deselects the investment" do
expect do
patch :deselect, xhr: true, params: { id: investment, budget_id: investment.budget }
end.to change { investment.reload.selected? }.from(true).to(false)
expect(response).to be_successful
end
it "does not modify non-selected investments" do
investment.update!(selected: false)
expect do
patch :deselect, xhr: true, params: { id: investment, budget_id: investment.budget }
end.not_to change { investment.reload.selected? }
end
it "uses the select/deselect authorization rules" do
investment.update!(valuation_finished: false)
patch :deselect, xhr: true, params: { id: investment, budget_id: investment.budget }
expect(flash[:alert]).to eq "You do not have permission to carry out the action " \
"'deselect' on Investment."
expect(investment).to be_selected
end
end
end end

View File

@@ -116,10 +116,10 @@ describe Abilities::Administrator do
it { should_not be_able_to(:valuate, finished_investment) } it { should_not be_able_to(:valuate, finished_investment) }
it { should_not be_able_to(:comment_valuation, finished_investment) } it { should_not be_able_to(:comment_valuation, finished_investment) }
it { should be_able_to(:toggle_selection, create(:budget_investment, :feasible, :finished)) } it { should be_able_to([:select, :deselect], create(:budget_investment, :feasible, :finished)) }
it { should_not be_able_to(:toggle_selection, create(:budget_investment, :feasible, :open)) } it { should_not be_able_to([:select, :deselect], create(:budget_investment, :feasible, :open)) }
it { should_not be_able_to(:toggle_selection, create(:budget_investment, :unfeasible, :finished)) } it { should_not be_able_to([:select, :deselect], create(:budget_investment, :unfeasible, :finished)) }
it { should_not be_able_to(:toggle_selection, finished_investment) } it { should_not be_able_to([:select, :deselect], finished_investment) }
it { should be_able_to(:destroy, proposal_image) } it { should be_able_to(:destroy, proposal_image) }
it { should be_able_to(:destroy, proposal_document) } it { should be_able_to(:destroy, proposal_document) }