Skip to content

Commit

Permalink
Front-end work for allowing users to switch organisations
Browse files Browse the repository at this point in the history
We have a new front-end component, a dropdown menu organisation switcher, which
will be displayed for a user with a non-zero number of
`addtional_organisations` and will allow them to switch their organisation.
(There is no functionality currently to populate a given user's
`additional_organisations` - this would need to be done manually. By default
a user will have zero `additional_organisations` and this component will be
hidden.)

Once switched, the user will see reports, exports, and activities for that
organisation. They can switch back to their original organisation in the
same way.

We store the user's switched organisation in the session as
`current_user_organisation`, and we also write this into
`Current.user_organisation` so we can easily access it in the `User` model
where we override the `#organisation` method in order to retrieve the data.
  • Loading branch information
benshimmin committed Dec 13, 2024
1 parent 2fa7106 commit e7a0490
Show file tree
Hide file tree
Showing 14 changed files with 270 additions and 8 deletions.
9 changes: 9 additions & 0 deletions app/controllers/application_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,15 @@ class ApplicationController < ActionController::Base
include Auth
include Ip

before_action -> {
return unless user_signed_in?

@organisation_list = current_user.all_organisations
if session[:current_user_organisation]
Current.user_organisation = session[:current_user_organisation]
end
}

private

def add_breadcrumb(name, path, options = {})
Expand Down
15 changes: 15 additions & 0 deletions app/controllers/organisation_session_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
class OrganisationSessionController < ApplicationController
include Secured

def update
desired_organisation_id = params[:current_user_organisation]

if desired_organisation_id
if current_user.additional_organisations.pluck(:id).include?(desired_organisation_id) ||
current_user.primary_organisation.id == desired_organisation_id
session[:current_user_organisation] = desired_organisation_id
end
end
redirect_to current_user.service_owner? ? request.referer : root_path
end
end
3 changes: 3 additions & 0 deletions app/views/layouts/application.html.haml
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,9 @@
= t("header.feedback.html")

= render 'layouts/messages'

= render "shared/organisation_switcher"

= breadcrumb_tags

= yield
Expand Down
8 changes: 4 additions & 4 deletions app/views/shared/_navigation.html.haml
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@
%li{ class: navigation_item_class(reports_path) }
= link_to t("page_title.report.index"), reports_path, class: "govuk-header__link"

%li{ class: navigation_item_class(organisation_activities_path(organisation_id: current_user.organisation_id)) }
= link_to t("page_title.activity.index"), organisation_activities_path(organisation_id: current_user.organisation_id), class: "govuk-header__link"
%li{ class: navigation_item_class(organisation_activities_path(organisation_id: current_user.organisation.id)) }
= link_to t("page_title.activity.index"), organisation_activities_path(organisation_id: current_user.organisation.id), class: "govuk-header__link"

- if policy(:level_b).budget_upload?
%li{ class: navigation_item_class(new_level_b_budgets_upload_path) }
Expand All @@ -20,8 +20,8 @@
%li{ class: navigation_item_class(exports_path) }
= link_to t("page_title.export.index"), exports_path, class: "govuk-header__link"
- elsif policy([:export, current_user.organisation]).show?
%li{ class: navigation_item_class(exports_organisation_path(id: current_user.organisation_id)) }
= link_to t("page_title.export.index"), exports_organisation_path(id: current_user.organisation_id), class: "govuk-header__link"
%li{ class: navigation_item_class(exports_organisation_path(id: current_user.organisation.id)) }
= link_to t("page_title.export.index"), exports_organisation_path(id: current_user.organisation.id), class: "govuk-header__link"

- if policy(Organisation).index?
%li{ class: navigation_item_class(organisations_path) }
Expand Down
7 changes: 7 additions & 0 deletions app/views/shared/_organisation_switcher.html.haml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
- if authenticated? && current_user.additional_organisations?
.govuk-grid-row
.govuk-grid-column-full{:class => "govuk-!-padding-top-4"}
=form_with url: organisation_session_path, method: :patch do |form|
=form.label t("organisation_switcher.label"), class: "govuk-label", for: "current_user_organisation"
=form.select :current_user_organisation, options_for_select(@organisation_list.pluck(:name, :id), Current.user_organisation || current_user.organisation.id), {}, class: "govuk-select"
=form.submit t("organisation_switcher.submit"), class: "govuk-button govuk-!-margin-bottom-1", "data-module": "govuk-button"
5 changes: 5 additions & 0 deletions config/locales/views/organisation_switcher.en.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
en:
organisation_switcher:
label: "Available organisations"
submit: "Set organisation"
2 changes: 2 additions & 0 deletions config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@
resources :users, except: [:index]
resources :activities, only: [:index]

patch "/organisation_session" => "organisation_session#update"

roles = %w[implementing_organisations partner_organisations matched_effort_providers external_income_providers]
constraints role: /#{roles.join("|")}/ do
get "organisations/(:role)", to: "organisations#index", defaults: {role: "partner_organisations"}, as: :organisations
Expand Down
1 change: 1 addition & 0 deletions spec/controllers/activities_controller_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,7 @@
before do
allow(controller).to receive(:current_user).and_return(user)
allow(ActivityPolicy).to receive(:new).and_return(policy)
allow(user).to receive(:all_organisations).and_return([])
end

describe "#confirm_destroy" do
Expand Down
47 changes: 47 additions & 0 deletions spec/controllers/application_controller_spec.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
require "rails_helper"

class DummyController < ApplicationController; end

RSpec.describe ApplicationController, type: :controller do
describe "#request_ip" do
it "returns the anonymized v4 IP with the last octet zero padded" do
Expand Down Expand Up @@ -27,4 +29,49 @@
end
end
end

describe "before_action" do
controller DummyController do
def custom_action
head 200
end
end

before(:each) do
routes.draw do
get "custom_action" => "dummy#custom_action"
end
end

context "user is not signed in" do
it "does not set Current.user_organisation" do
get "custom_action"

expect(Current.user_organisation).to be(nil)
end
end

context "user is signed in" do
let(:user) { create(:partner_organisation_user) }

before do
allow(controller).to receive(:current_user).and_return(user)
end

it "does not set Current.user_organisation if `current_user_organisation` is not in the session" do
get "custom_action"

expect(Current.user_organisation).to be(nil)
end

it "sets Current.user_organisation if `current_user_organisation` is in the session" do
id = SecureRandom.uuid
session[:current_user_organisation] = id

get "custom_action"

expect(Current.user_organisation).to be(id)
end
end
end
end
47 changes: 47 additions & 0 deletions spec/controllers/organisation_session_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
require "rails_helper"

RSpec.describe OrganisationSessionController do
let(:user) { create(:partner_organisation_user, organisation: organisation) }
let(:organisation) { create(:partner_organisation) }
let(:other_organisation) { create(:partner_organisation) }

before do
allow(controller).to receive(:current_user).and_return(user)

user.additional_organisations << [create(:partner_organisation), create(:partner_organisation)]
end

describe "#update" do
it "sets the session `current_user_organisation` to the user's primary organisation's ID" do
put :update, params: build_params(user.primary_organisation.id)

expect(session[:current_user_organisation]).to eq(user.primary_organisation.id)
end

it "sets the session `current_user_organisation` to an ID from the user's additional organisations" do
id = user.additional_organisations.pluck(:id).sample

put :update, params: build_params(id)

expect(session[:current_user_organisation]).to eq(id)
end

it "does not set the session `current_user_organisation` to a random ID" do
random_id = SecureRandom.uuid

put :update, params: build_params(random_id)

expect(session[:current_user_organisation]).not_to eq(random_id)
end

it "does not set the session `current_user_organisation` to an organisation ID not in the user's additional organisations" do
put :update, params: build_params(other_organisation.id)

expect(session[:current_user_organisation]).not_to eq(other_organisation.id)
end

def build_params(id)
{current_user_organisation: id}
end
end
end
6 changes: 5 additions & 1 deletion spec/features/users_can_edit_a_comment_spec.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
RSpec.describe "Users can edit a comment" do
include HideFromBullet

let(:beis_user) { create(:beis_user) }
let(:partner_org_user) { create(:partner_organisation_user) }

Expand Down Expand Up @@ -77,7 +79,9 @@
scenario "the user can edit comments on actuals belonging to the same organisation" do
actual = create(:actual, :with_comment, report: project_activity_report, parent_activity: project_activity)

visit organisation_activity_comments_path(project_activity_comment.commentable.organisation, project_activity_comment.commentable)
skip_bullet do
visit organisation_activity_comments_path(project_activity_comment.commentable.organisation, project_activity_comment.commentable)
end

expect(page).to have_link("Edit", href: edit_activity_actual_path(project_activity, actual))
end
Expand Down
116 changes: 116 additions & 0 deletions spec/features/users_can_switch_organisation_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
RSpec.feature "Users can switch organisation" do
context "when the user has additional organisations" do
let(:user_with_additional_organisations) do
org1 = create(:partner_organisation)
org2 = create(:partner_organisation)
user = create(:partner_organisation_user)
user.additional_organisations << [org1, org2]
user
end

before do
authenticate!(user: user_with_additional_organisations)
visit root_path
end
after { logout }

scenario "the organisation switcher dropdown is shown" do
expect(page).to have_content t("organisation_switcher.label")
end

scenario "the organisation switcher dropdown is populated correctly" do
user_with_additional_organisations.all_organisations.each do |org|
expect(page).to have_content org.name
end
end

scenario "the user can switch organisation" do
expect(page).to have_select("current_user_organisation", selected: user_with_additional_organisations.primary_organisation.name)

additional_org_name = user_with_additional_organisations.additional_organisations.first.name

select(additional_org_name, from: "current_user_organisation")
click_on t("organisation_switcher.submit")

expect(page).to have_select("current_user_organisation", selected: additional_org_name)
end

scenario "the nav links have the correct organisation ID when the user has switched organisation" do
additional_org = user_with_additional_organisations.additional_organisations.first

activities_href = organisation_activities_path(organisation_id: user_with_additional_organisations.primary_organisation.id)
exports_href = exports_organisation_path(id: user_with_additional_organisations.primary_organisation.id)

expect(page).to have_link(href: activities_href)
expect(page).to have_link(href: exports_href)

select(additional_org.name, from: "current_user_organisation")
click_on t("organisation_switcher.submit")

updated_activities_href = organisation_activities_path(organisation_id: additional_org.id)
updated_exports_href = exports_organisation_path(id: additional_org.id)

expect(page).to have_link(href: updated_activities_href)
expect(page).to have_link(href: updated_exports_href)
end

scenario "the Activities page shows the correct content when the user has switched organisation" do
additional_org = user_with_additional_organisations.additional_organisations.first

select(additional_org.name, from: "current_user_organisation")
click_on t("organisation_switcher.submit")

updated_activities_href = organisation_activities_path(organisation_id: additional_org.id)

visit(updated_activities_href)

expect(page).to have_content(t("page_title.activity.index"))
end

scenario "the Exports page shows the correct content when the user has switched organisation" do
additional_org = user_with_additional_organisations.additional_organisations.first

select(additional_org.name, from: "current_user_organisation")
click_on t("organisation_switcher.submit")

updated_exports_href = exports_organisation_path(id: additional_org.id)

visit(updated_exports_href)

expect(page).to have_content(t("page_title.export.organisation.show", name: additional_org.name))
end

scenario "the nav links have the primary organisation ID when the user has switched organisation and switched back" do
activities_href = organisation_activities_path(organisation_id: user_with_additional_organisations.primary_organisation.id)
exports_href = exports_organisation_path(id: user_with_additional_organisations.primary_organisation.id)

additional_org = user_with_additional_organisations.additional_organisations.first

select(additional_org.name, from: "current_user_organisation")
click_on t("organisation_switcher.submit")

updated_exports_href = exports_organisation_path(id: additional_org.id)

visit(updated_exports_href)

select(user_with_additional_organisations.primary_organisation.name, from: "current_user_organisation")
click_on t("organisation_switcher.submit")

expect(page).to have_link(href: activities_href)
expect(page).to have_link(href: exports_href)
end
end

context "when the user has no additional organisations" do
let(:user) { create(:partner_organisation_user) }

before { authenticate!(user:) }
after { logout }

scenario "the organisation switcher dropdown is not shown" do
visit root_path

expect(page).not_to have_content t("organisation_switcher.label")
end
end
end
2 changes: 2 additions & 0 deletions spec/rails_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -83,10 +83,12 @@
ActionMailer::Base.delivery_method = :test
ActionMailer::Base.perform_deliveries = true
ActionMailer::Base.deliveries = []
ActiveSupport::CurrentAttributes.reset_all
end

config.after(:each) do |example|
ActionMailer::Base.deliveries.clear
ActiveSupport::CurrentAttributes.reset_all
end

config.before(:each, type: :request) do
Expand Down
10 changes: 7 additions & 3 deletions spec/views/shared/navigation_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,34 +6,38 @@
allow(view).to receive(:policy) do |record|
Pundit.policy(user, record)
end

allow(view).to receive(:current_user).and_return(user)
allow(user).to receive(:organisation_id).and_return(SecureRandom.uuid)
allow(user).to receive(:organisation).and_return(organisation)
allow(organisation).to receive(:id).and_return(SecureRandom.uuid)
end

render
end

context "when the current user is a BEIS user" do
let(:user) { build(:beis_user) }
let(:organisation) { build(:beis_organisation) }

it "shows the link to the exports index" do
expect(rendered).to have_link(t("page_title.export.index"), href: exports_path)
end

it "does not show the link to an organisation export page" do
expect(rendered).to_not have_link(t("page_title.export.index"), href: exports_organisation_path(id: user.organisation_id))
expect(rendered).to_not have_link(t("page_title.export.index"), href: exports_organisation_path(id: user.organisation.id))
end
end

context "when the current user is a partner organisation user" do
let(:user) { build(:partner_organisation_user) }
let(:organisation) { build(:partner_organisation) }

it "does not show the link to the exports index" do
expect(rendered).to_not have_link(t("page_title.export.index"), href: exports_path)
end

it "shows the link to the partner organisation's organisation export page" do
expect(rendered).to have_link(t("page_title.export.index"), href: exports_organisation_path(id: user.organisation_id))
expect(rendered).to have_link(t("page_title.export.index"), href: exports_organisation_path(id: user.organisation.id))
end
end
end

0 comments on commit e7a0490

Please sign in to comment.