Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Administrative interface for user additional organisations #2485

Open
wants to merge 1 commit into
base: develop
Choose a base branch
from
Open
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
15 changes: 10 additions & 5 deletions app/controllers/users_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,12 @@ def new
end

def create
@user = User.new(user_params.except(:active))
@user = User.new(user_params.except(:active, :additional_organisations))
authorize @user
@service_owner = service_owner
@partner_organisations = partner_organisations

result = CreateUser.new(user: @user, organisation: organisation).call
result = CreateUser.new(user: @user, organisation:, additional_organisations:).call
if result.success?
flash.now[:notice] = t("action.user.create.success")
redirect_to user_path(@user.reload.id)
Expand All @@ -60,10 +60,11 @@ def update

reset_mfa = user_params.delete(:reset_mfa)
active = user_params[:active] == "true"
@user.assign_attributes(user_params.except(:reset_mfa, :active))
@user.assign_attributes(user_params.except(:reset_mfa, :active, :additional_organisations))
@user.additional_organisations = additional_organisations

if @user.valid?
result = UpdateUser.new(user: @user, active: active, organisation: organisation, reset_mfa: reset_mfa).call
result = UpdateUser.new(user: @user, active: active, organisation:, reset_mfa:, additional_organisations:).call

if result.success?
flash[:notice] = t("action.user.update.success")
Expand All @@ -78,7 +79,7 @@ def update
end

def user_params
params.require(:user).permit(:name, :email, :organisation_id, :active, :reset_mfa)
params.require(:user).permit(:name, :email, :organisation_id, :active, :reset_mfa, additional_organisations: [])
end

def id
Expand All @@ -93,6 +94,10 @@ def organisation
Organisation.find_by(id: organisation_id)
end

def additional_organisations
user_params[:additional_organisations].reject(&:blank?).map { |id| Organisation.find_by(id:) }
end

private def service_owner
Organisation.service_owner
end
Expand Down
1 change: 1 addition & 0 deletions app/javascript/application.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import "./src/cookie-consent";
import initTableTreeView from "./src/table-tree-view";
import "./src/toggle-providing-org-fields";
import "./src/region-countries-checkbox";
import "./src/edit-user-additional-organisations";
import * as GOVUKFrontend from "govuk-frontend";

document.addEventListener("DOMContentLoaded", () => {
Expand Down
23 changes: 23 additions & 0 deletions app/javascript/src/edit-user-additional-organisations.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/*
This progressively enhances the "Create/Edit user" form such that a primary
organisation will be hidden from the list of additional organisations (and
also unchecked), because a user's primary organisation can never be a member
of that user's additional organisations.
*/
document.addEventListener("DOMContentLoaded", function() {
const primaryOrgSelect = document.querySelector("#user_organisation_id");

if (!primaryOrgSelect) return;

const handleCheckboxes = () => {
const val = primaryOrgSelect.querySelector("option:checked").value;

document.querySelectorAll(".additional-organisations .govuk-checkboxes__item").forEach((checkboxItem) => {
const match = checkboxItem.querySelector(`input[value="${val}"`);
checkboxItem.style.display = match ? (match.checked = false, "none") : "block";
});
}

primaryOrgSelect.addEventListener("change", handleCheckboxes);
handleCheckboxes();
});
3 changes: 2 additions & 1 deletion app/models/user.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ class User < ApplicationRecord
has_many :historical_events
validates_presence_of :name, :email
validates :email, with: :email_cannot_be_changed_after_create, on: :update
validates :organisation_id, exclusion: {in: ->(user) { user.additional_organisations.map(&:id) }}

before_save :ensure_otp_secret!, if: -> { otp_required_for_login && otp_secret.nil? }

Expand Down Expand Up @@ -39,7 +40,7 @@ def organisation
end

def primary_organisation
Organisation.find(organisation_id)
Organisation.find_by_id(organisation_id)
end

def all_organisations
Expand Down
6 changes: 4 additions & 2 deletions app/services/create_user.rb
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
class CreateUser
attr_accessor :user, :organisation
attr_accessor :user, :organisation, :additional_organisations

def initialize(user:, organisation:)
def initialize(user:, organisation:, additional_organisations: [])
self.user = user
self.organisation = organisation
self.additional_organisations = additional_organisations
end

def call
result = Result.new(true)

user.organisation = organisation
user.additional_organisations = additional_organisations
# This password will never be used: the user must set a new password upon first login
# but it must fulfill the password_complexity requirements we set in
# config/initializers/devise_security
Expand Down
6 changes: 4 additions & 2 deletions app/services/update_user.rb
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
class UpdateUser
attr_accessor :user, :organisation, :reset_mfa
attr_accessor :user, :organisation, :reset_mfa, :additional_organisations

def initialize(user:, organisation:, active: true, reset_mfa: false)
def initialize(user:, organisation:, active: true, reset_mfa: false, additional_organisations: [])
self.user = user
self.organisation = organisation
self.reset_mfa = reset_mfa
self.additional_organisations = additional_organisations
@active = active
end

Expand All @@ -13,6 +14,7 @@ def call

User.transaction do
user.organisation = organisation
user.additional_organisations = additional_organisations

if reset_mfa
user.mobile_number = nil
Expand Down
17 changes: 11 additions & 6 deletions app/views/users/_form.html.haml
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,23 @@
= f.govuk_check_box :reset_mfa, true, multiple: false, label: { text: t("form.label.user.reset_mfa") }

- if @service_owner.present?
= f.govuk_radio_buttons_fieldset :organisation, class: "user-organisations" do
= f.govuk_radio_button :organisation_id, @service_owner.id, label: { text: @service_owner.name }, link_errors: true
- if @partner_organisations.any?
= f.govuk_radio_divider
- @partner_organisations.each do |dp|
= f.govuk_radio_button :organisation_id, dp.id, label: { text: dp.name }
.govuk-form-group
= f.govuk_fieldset legend: {text: t("form.legend.user.primary_organisation")}, class: "user-organisations" do
%span.govuk-hint=t("form.hint.user.primary_organisation")

- opts = [[@service_owner.name, @service_owner.id]] + @partner_organisations.pluck(:name, :id)
=f.select :organisation_id, options_for_select(opts, @user.primary_organisation.try(:id)), {}, class: "govuk-select"

- else
.govuk-inset-text
= succeed "." do
= t("page_content.users.new.no_organisations.cta")
= link_to t("page_content.users.new.no_organisations.link"), new_organisation_path, class: "govuk-link"

= f.govuk_check_boxes_fieldset :additional_organisations, legend: {text: "Additional organisations"}, class: "additional-organisations", hint: {text: t("form.hint.user.additional_organisations")} do
- @partner_organisations.each do |dp|
= f.govuk_check_box :additional_organisations, dp.id, label: { text: dp.name }, checked: @user.additional_organisations.include?(dp)

= f.govuk_collection_radio_buttons :active,
user_active_options,
:id,
Expand Down
6 changes: 6 additions & 0 deletions app/views/users/show.html.haml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,12 @@
= t("summary.label.user.organisation")
%dd.govuk-summary-list__value
= @user.organisation.name
- if @user.additional_organisations?
.govuk-summary-list__row
%dt.govuk-summary-list__key
= t("summary.label.user.additional_organisations")
%dd.govuk-summary-list__value
= @user.additional_organisations.map(&:name).to_sentence
.govuk-summary-list__row
%dt.govuk-summary-list__key
= t("summary.label.user.active")
Expand Down
6 changes: 6 additions & 0 deletions config/locales/models/user.en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,14 @@ en:
active: What is the user's status?
organisation_id: What organisation does this user belong to?
reset_mfa: Reset the user's mobile number?
primary_organisation: Primary organisation
hint:
user:
active: Deactivated users cannot log in
new_password: Minimum 15 characters; must contain at least one digit, one lowercase letter, one uppercase letter, and one punctuation mark or symbol
reset_mfa: The user will have to provide their mobile number on their next log in attempt
primary_organisation: This is the main organisation the user belongs to
additional_organisations: Select one or more additional organisations that this user can supply data on behalf of. They will not receive notifications for these organisations
user:
active:
active: Activate
Expand All @@ -48,6 +51,7 @@ en:
name: Full name
email: Email address
organisation: Organisation
additional_organisations: Additional organisations
active: Active?
confirmed_for_mfa:
label: Mobile number confirmed for authentication?
Expand Down Expand Up @@ -88,6 +92,8 @@ en:
attributes:
organisation:
required: Select the user's organisation
organisation_id:
exclusion: Additional organisations cannot include the primary organisation.
name:
blank: Enter a full name
email:
Expand Down
32 changes: 32 additions & 0 deletions spec/features/beis_users_can_edit_a_user_spec.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
require "rails_helper"

RSpec.feature "BEIS users can edit other users" do
include HideFromBullet

let!(:user) { create(:partner_organisation_user, organisation: create(:partner_organisation)) }
after { logout }

Expand Down Expand Up @@ -99,6 +101,36 @@
expect(user.reload.active).to be(true)
end

scenario "a user can have additional organisations" do
administrator_user = create(:beis_user)
authenticate!(user: administrator_user)

target_user = create(:partner_organisation_user)

additional_orgs = []

# Navigate to the users page
skip_bullet do
visit users_index_path(user_state: "active")

find("tr", text: target_user.name).click_link("Edit")

# Set a couple of random partner organisations which *aren't* the
# primary organisation
Organisation.partner_organisations
.reject { |org| org.id == target_user.primary_organisation.id }
.pluck(:name).sample(3).each do |org|
additional_orgs << org
check org
end

click_on t("default.button.submit")
end

expect(page).to have_content("User successfully updated")
expect(page).to have_content(additional_orgs.to_sentence)
end

context "editing a user with a non-lowercase email address" do
before do
# Given a non-lowercase email address exists
Expand Down
19 changes: 13 additions & 6 deletions spec/features/beis_users_can_invite_new_users_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,17 @@
scenario "a new user can be created" do
organisation = create(:partner_organisation)
second_organisation = create(:partner_organisation)
additional_organisation = create(:partner_organisation)
new_user_name = "Foo Bar"
new_user_email = "[email protected]"

perform_enqueued_jobs do
create_user(organisation, new_user_name, new_user_email)
create_user(organisation, additional_organisation, new_user_name, new_user_email)
end

expect(page).to have_content(organisation.name)
expect(page).not_to have_content(second_organisation.name)
expect(page).to have_content(additional_organisation.name)

new_user = User.where(email: new_user_email).first
reset_password_link_regex = %r{http://test.local/users/password/edit\?reset_password_token=.*}
Expand Down Expand Up @@ -56,7 +58,7 @@
end
end

def create_user(organisation, new_user_name, new_user_email)
def create_user(organisation, additional_organisation, new_user_name, new_user_email)
# Navigate from the landing page
visit organisation_path(organisation)
click_on("Users")
Expand All @@ -67,19 +69,24 @@ def create_user(organisation, new_user_name, new_user_email)
# Create a new user
click_on("Add user")

# We expect to see BEIS separately on this page
# We expect to see BEIS on this page in the dropdown
within(".user-organisations") do
beis_identifier = Organisation.service_owner.id
expect(page).to have_css("input[type='radio'][value='#{beis_identifier}']:first-child")
expect(page).to have_css(".govuk-radios__divider:nth-child(2)")
expect(page).to have_css("select option[value='#{beis_identifier}']")
end

# We expect to see the additional organisation too
within(".additional-organisations") do
expect(page).to have_css("input[value='#{additional_organisation.id}']")
end

# Fill out the form
expect(page).not_to have_content("Reset the user's mobile number?")
expect(page).to have_content("Create user")
fill_in "user[name]", with: new_user_name
fill_in "user[email]", with: new_user_email
choose organisation.name
select organisation.name
check additional_organisation.name

# Submit the form
click_button "Submit"
Expand Down
8 changes: 8 additions & 0 deletions spec/models/user_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,14 @@

expect(user).to be_valid
end

it "won't allow a user to have its primary organisation also as an additional organisation" do
user = create(:administrator)
org = user.organisation
user.organisation = org
user.additional_organisations << org
expect(user).to be_invalid
end
end

describe "associations" do
Expand Down
16 changes: 16 additions & 0 deletions spec/services/create_user_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -33,5 +33,21 @@
expect(user.reload.organisation).to eql(organisation)
end
end

context "when additional organisations are provided" do
it "associates the additional organsations to it" do
organisation = create(:partner_organisation)
org1 = create(:partner_organisation)
org2 = create(:partner_organisation)

described_class.new(
user: user,
organisation: organisation,
additional_organisations: [org1, org2]
).call

expect(user.reload.additional_organisations).to include(org1, org2)
end
end
end
end
16 changes: 16 additions & 0 deletions spec/services/update_user_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,22 @@
end
end

context "when additional organisations are provided" do
it "associates the additional organsations to it" do
organisation = create(:partner_organisation)
org1 = create(:partner_organisation)
org2 = create(:partner_organisation)

described_class.new(
user: user,
organisation: organisation,
additional_organisations: [org1, org2]
).call

expect(user.reload.additional_organisations).to include(org1, org2)
end
end

context "when reset MFA is requested" do
it "resets the user's mobile number and its confirmation time" do
described_class.new(
Expand Down
Loading