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.

(We need to include `HideFromBullet` in the user comment edit spec because
`additional_organisations` on the `User` model confuses Bullet.)
  • Loading branch information
benshimmin committed Dec 16, 2024
1 parent 1819420 commit ff1ae69
Show file tree
Hide file tree
Showing 17 changed files with 287 additions and 8 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

[Full changelog][unreleased]

- Dropdown switcher component for users with multiple organisations

## Release 156 - 2024-12-12

[Full changelog][156]
Expand Down
11 changes: 11 additions & 0 deletions app/controllers/application_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,19 @@ class ApplicationController < ActionController::Base
include Auth
include Ip

before_action :set_organisation_list_and_current_organisation

private

def set_organisation_list_and_current_organisation
return unless user_signed_in?

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

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

def update
desired_organisation_id = params[:current_user_organisation]

if desired_organisation_id
if current_user.all_organisations.pluck(:id).include?(desired_organisation_id)
session[:current_user_organisation] = desired_organisation_id
end
end
redirect_to current_user.service_owner? ? request.referer : root_path
end
end
4 changes: 4 additions & 0 deletions app/models/user.rb
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,10 @@ def additional_organisations?
additional_organisations.any?
end

def current_organisation_id
Current.user_organisation || organisation.id
end

def active_for_authentication?
active
end
Expand Down
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: users_session_organisation_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.current_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"
4 changes: 4 additions & 0 deletions config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@
resources :users, except: [:index]
resources :activities, only: [:index]

namespace :users do
patch "session/organisation" => "organisation_session#update", :as => :session_organisation
end

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
8 changes: 8 additions & 0 deletions spec/models/user_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -88,13 +88,21 @@
user = create(:administrator)

expect(user.organisation).to eq(current_organisation)
expect(user.current_organisation_id).to eq(current_organisation.id)
expect(user.primary_organisation).not_to eq(current_organisation)
end

after do
Current.user_organisation = nil
end
end

context "when the current organisation has not been set" do
it "returns the primary organisation" do
user = create(:administrator)
expect(user.current_organisation_id).to eq(user.organisation.id)
end
end
end

describe "delegations" do
Expand Down
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
Loading

0 comments on commit ff1ae69

Please sign in to comment.