Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion app/controllers/allocations_controller.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
class AllocationsController < ApplicationController
include ScenarioScoping

before_action :set_scenario

def create
Expand Down Expand Up @@ -27,7 +29,7 @@ def destroy
private

def set_scenario
@scenario = Current.user.scenarios.where(organization: Current.organization).find(params[:scenario_id])
@scenario = accessible_scenarios.find(params[:scenario_id])
end

def allocation_params
Expand Down
21 changes: 21 additions & 0 deletions app/controllers/concerns/scenario_scoping.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
module ScenarioScoping
extend ActiveSupport::Concern

included do
helper_method :viewing_on_behalf?
end

private

def accessible_scenarios
Current.organization_membership&.accessible_scenarios || owned_scenarios
end

def owned_scenarios
Current.user.scenarios.where(organization: Current.organization)
end

def viewing_on_behalf?(scenario)
scenario.present? && scenario.user_id != Current.user.id
end
end
4 changes: 3 additions & 1 deletion app/controllers/scenarios/names_controller.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
class Scenarios::NamesController < ApplicationController
include ScenarioScoping

before_action :set_scenario

def show
Expand All @@ -18,7 +20,7 @@ def update
private

def set_scenario
@scenario = Current.user.scenarios.where(organization: Current.organization).find(params[:scenario_id])
@scenario = accessible_scenarios.find(params[:scenario_id])
end

def name_params
Expand Down
4 changes: 3 additions & 1 deletion app/controllers/scenarios/total_giving_amounts_controller.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
class Scenarios::TotalGivingAmountsController < ApplicationController
include ScenarioScoping

before_action :set_scenario

def show
Expand All @@ -18,7 +20,7 @@ def update
private

def set_scenario
@scenario = Current.user.scenarios.where(organization: Current.organization).find(params[:scenario_id])
@scenario = accessible_scenarios.find(params[:scenario_id])
end

def total_giving_amount_params
Expand Down
17 changes: 8 additions & 9 deletions app/controllers/scenarios_controller.rb
Original file line number Diff line number Diff line change
@@ -1,19 +1,21 @@
class ScenariosController < ApplicationController
include ScenarioScoping

before_action :set_scenario, only: %i[ show update destroy ]

def index
@scenarios = scenarios.order(created_at: :desc)
@scenarios = owned_scenarios.order(created_at: :desc)
end

def show
end

def new
@scenario = scenarios.new
@scenario = owned_scenarios.new
end

def create
@scenario = scenarios.new(scenario_params)
@scenario = owned_scenarios.new(scenario_params)
if @scenario.save
redirect_to scenario_path(@scenario)
else
Expand All @@ -30,18 +32,15 @@ def update
end

def destroy
on_behalf = viewing_on_behalf?(@scenario)
@scenario.destroy
redirect_to scenarios_path
redirect_to on_behalf ? admin_scenarios_path : scenarios_path
end

private

def scenarios
Current.user.scenarios.where(organization: Current.organization)
end

def set_scenario
@scenario = scenarios.find(params[:id])
@scenario = accessible_scenarios.find(params[:id])
end

def scenario_params
Expand Down
4 changes: 4 additions & 0 deletions app/models/current.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,8 @@ class Current < ActiveSupport::CurrentAttributes
attribute :session
attribute :organization
delegate :user, to: :session, allow_nil: true

def organization_membership
user&.membership_in(organization)
end
end
12 changes: 12 additions & 0 deletions app/models/organization_membership.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,16 @@ class OrganizationMembership < ApplicationRecord

validates :user_id, uniqueness: { scope: :organization_id }
validates :role, presence: true

def accessible_scenarios
if admin? || owner?
organization.scenarios
else
owned_scenarios
end
end

def owned_scenarios
user.scenarios.where(organization: organization)
end
end
8 changes: 6 additions & 2 deletions app/views/admin/scenarios/index.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,13 @@
data: { turbo_frame: "_top" },
class: "text-ink hover:text-ink-soft hover:underline" %>
</td>
<td class="px-4 py-2.5 text-ink-soft"><%= scenario.name %></td>
<td class="px-4 py-2.5 text-ink-soft">
<%= link_to scenario.name, scenario_path(scenario), data: { turbo_frame: "_top" }, class: "block hover:underline" %>
</td>
<td class="px-4 py-2.5 text-right tabular-nums text-ink">
<%= scenario.total_giving_amount.present? ? number_to_currency(scenario.total_giving_amount, precision: 0) : content_tag(:span, "—", class: "text-ink-faint") %>
<%= link_to scenario_path(scenario), data: { turbo_frame: "_top" }, class: "block" do %>
<%= scenario.total_giving_amount.present? ? number_to_currency(scenario.total_giving_amount, precision: 0) : content_tag(:span, "—", class: "text-ink-faint") %>
<% end %>
</td>
</tr>
<% end %>
Expand Down
15 changes: 13 additions & 2 deletions app/views/scenarios/show.html.erb
Original file line number Diff line number Diff line change
@@ -1,6 +1,17 @@
<div class="mx-auto max-w-5xl w-full px-4 py-10">
<%= link_to "← Back to explore options", scenarios_path,
class: "inline-block rounded-md border border-line bg-surface px-3 py-1.5 text-sm text-ink-soft hover:bg-canvas transition" %>
<% if viewing_on_behalf?(@scenario) %>
<%= link_to "← Back to all scenarios", admin_scenarios_path,
class: "inline-block rounded-md border border-line bg-surface px-3 py-1.5 text-sm text-ink-soft hover:bg-canvas transition" %>
<% else %>
<%= link_to "← Back to explore options", scenarios_path,
class: "inline-block rounded-md border border-line bg-surface px-3 py-1.5 text-sm text-ink-soft hover:bg-canvas transition" %>
<% end %>

<% if viewing_on_behalf?(@scenario) %>
<div class="mt-4 rounded-md border border-line bg-surface-soft px-4 py-2.5 text-sm text-ink-soft">
Viewing <span class="font-medium text-ink"><%= @scenario.user.display_name %></span>'s scenario as an admin. Changes you make are saved to their account.
</div>
<% end %>

<%= render "scenarios/names/name", scenario: @scenario %>

Expand Down
9 changes: 9 additions & 0 deletions test/controllers/admin/scenarios_controller_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,15 @@ class Admin::ScenariosControllerTest < ActionDispatch::IntegrationTest
assert_select "td", text: @admin.display_name
end

test "rows link to the scenario and break out of the table turbo frame" do
sign_in_as(@owner)
get admin_scenarios_path

# data-turbo-frame="_top" is required so the link navigates the whole page
# instead of trying (and failing) to render into the scenarios_table frame.
assert_select "a[href=?][data-turbo-frame=_top]", scenario_path(scenarios(:one_arlington))
end

test "does not show scenarios from another organization" do
sign_in_as(@owner)
get admin_scenarios_path
Expand Down
28 changes: 28 additions & 0 deletions test/controllers/allocations_controller_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -103,4 +103,32 @@ class AllocationsControllerTest < ActionDispatch::IntegrationTest
end
assert_response :not_found
end

test "admin can add an allocation to another user's scenario in the same org" do
sign_in_as users(:admin)
assert_difference -> { @scenario.allocations.count }, 1 do
post scenario_allocations_url(@scenario), params: {
allocation: { type: "Allocation::Ongoing", option: "Population: Youth", percentage: 25 }
}
end
assert_redirected_to scenario_path(@scenario)
end

test "admin can destroy an allocation on another user's scenario in the same org" do
sign_in_as users(:admin)
assert_difference -> { @scenario.allocations.count }, -1 do
delete scenario_allocation_url(@scenario, allocations(:greatest_need))
end
assert_redirected_to scenario_path(@scenario)
end

test "plain member cannot add an allocation to another user's scenario in the same org" do
sign_in_as users(:passwordless)
assert_no_difference -> { Allocation.count } do
post scenario_allocations_url(@scenario), params: {
allocation: { type: "Allocation::Ongoing", option: "X", percentage: 10 }
}
end
assert_response :not_found
end
end
16 changes: 16 additions & 0 deletions test/controllers/scenarios/names_controller_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,20 @@ class Scenarios::NamesControllerTest < ActionDispatch::IntegrationTest
get edit_scenario_name_url(scenarios(:two_boston))
assert_response :not_found
end

test "admin can edit another user's scenario name in the same org" do
sign_in_as users(:admin)
get edit_scenario_name_url(@scenario)
assert_response :success

patch scenario_name_url(@scenario), params: { scenario: { name: "Admin renamed" } }
assert_response :success
assert_equal "Admin renamed", @scenario.reload.name
end

test "plain member cannot edit another user's scenario name in the same org" do
sign_in_as users(:passwordless)
get edit_scenario_name_url(@scenario)
assert_response :not_found
end
end
16 changes: 16 additions & 0 deletions test/controllers/scenarios/total_giving_amounts_controller_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,20 @@ class Scenarios::TotalGivingAmountsControllerTest < ActionDispatch::IntegrationT
get edit_scenario_total_giving_amount_url(scenarios(:two_boston))
assert_response :not_found
end

test "admin can edit another user's scenario total in the same org" do
sign_in_as users(:admin)
get edit_scenario_total_giving_amount_url(@scenario)
assert_response :success

patch scenario_total_giving_amount_url(@scenario), params: { scenario: { total_giving_amount: 7500 } }
assert_response :success
assert_equal 7500, @scenario.reload.total_giving_amount
end

test "plain member cannot edit another user's scenario total in the same org" do
sign_in_as users(:passwordless)
get edit_scenario_total_giving_amount_url(@scenario)
assert_response :not_found
end
end
41 changes: 41 additions & 0 deletions test/controllers/scenarios_controller_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -63,4 +63,45 @@ class ScenariosControllerTest < ActionDispatch::IntegrationTest
get scenario_url(scenarios(:two_boston))
assert_response :not_found
end

test "admin can view another user's scenario in the same org" do
sign_in_as users(:admin)
get scenario_url(scenarios(:one_arlington))
assert_response :success
assert_select "a[href=?]", admin_scenarios_path
assert_match "as an admin", response.body
end

test "admin can update another user's scenario" do
sign_in_as users(:admin)
patch scenario_url(scenarios(:one_arlington)), params: { scenario: { total_giving_amount: 999 } }
assert_equal 999, scenarios(:one_arlington).reload.total_giving_amount
end

test "admin destroy of another user's scenario returns to the admin dashboard" do
sign_in_as users(:admin)
assert_difference -> { Scenario.count }, -1 do
delete scenario_url(scenarios(:one_arlington))
end
assert_redirected_to admin_scenarios_path
end

test "admin still cannot reach a scenario in another org" do
sign_in_as users(:admin)
get scenario_url(scenarios(:two_boston))
assert_response :not_found
end

test "plain member cannot reach another user's scenario in the same org" do
sign_in_as users(:passwordless)
get scenario_url(scenarios(:one_arlington))
assert_response :not_found
end

test "super admin cannot change another user's scenario" do
sign_in_as users(:super_admin)
patch scenario_url(scenarios(:one_arlington)), params: { scenario: { total_giving_amount: 1 } }
assert_response :not_found
assert_equal 10000, scenarios(:one_arlington).reload.total_giving_amount
end
end
20 changes: 20 additions & 0 deletions test/models/organization_membership_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -37,4 +37,24 @@ class OrganizationMembershipTest < ActiveSupport::TestCase
assert_not membership.valid?
assert_includes membership.errors[:role], "can't be blank"
end

test "owned_scenarios are the member's own scenarios in the organization" do
membership = organization_memberships(:one_arlington)
assert_includes membership.owned_scenarios, scenarios(:one_arlington)
assert_not_includes membership.owned_scenarios, scenarios(:admin_arlington)
end

test "admins and owners can access any scenario in the organization" do
%i[ admin_arlington one_arlington ].each do |fixture|
membership = organization_memberships(fixture)
assert_includes membership.accessible_scenarios, scenarios(:one_arlington)
assert_includes membership.accessible_scenarios, scenarios(:admin_arlington)
end
end

test "plain members can only access their own scenarios" do
membership = organization_memberships(:passwordless_arlington)
assert_equal membership.owned_scenarios.to_a, membership.accessible_scenarios.to_a
assert_not_includes membership.accessible_scenarios, scenarios(:one_arlington)
end
end
Loading