diff --git a/Gemfile b/Gemfile
index 1e6a5d305a..2e27815658 100644
--- a/Gemfile
+++ b/Gemfile
@@ -89,6 +89,8 @@ group :test do
gem "rails-controller-testing"
gem "selenium-webdriver"
gem "shoulda-matchers"
+ # launch browser when inspecting capybara specs
+ gem "launchy"
end
group :test, :development do
diff --git a/Gemfile.lock b/Gemfile.lock
index 6314b9ca5e..25336e1a4e 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -205,6 +205,8 @@ GEM
kramdown-parser-gfm (1.1.0)
kramdown (~> 2.0)
language_server-protocol (3.17.0.3)
+ launchy (2.5.2)
+ addressable (~> 2.8)
loofah (2.22.0)
crass (~> 1.0.2)
nokogiri (>= 1.12.0)
@@ -525,6 +527,7 @@ DEPENDENCIES
govuk-components (~> 5.0.0)
govuk_design_system_formbuilder (~> 5.0.0)
jsbundling-rails
+ launchy
mail-notify
omniauth
omniauth-rails_csrf_protection
diff --git a/app/controllers/claims/organisations_controller.rb b/app/controllers/claims/organisations_controller.rb
new file mode 100644
index 0000000000..ca229cc8fd
--- /dev/null
+++ b/app/controllers/claims/organisations_controller.rb
@@ -0,0 +1,5 @@
+class Claims::OrganisationsController < ApplicationController
+ def index
+ @schools = current_user.schools
+ end
+end
diff --git a/app/controllers/placements/organisations_controller.rb b/app/controllers/placements/organisations_controller.rb
new file mode 100644
index 0000000000..3b848684eb
--- /dev/null
+++ b/app/controllers/placements/organisations_controller.rb
@@ -0,0 +1,6 @@
+class Placements::OrganisationsController < ApplicationController
+ def index
+ @schools = current_user.schools
+ @providers = current_user.providers
+ end
+end
diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb
index 9a2884fc70..329e153ac7 100644
--- a/app/controllers/sessions_controller.rb
+++ b/app/controllers/sessions_controller.rb
@@ -19,4 +19,14 @@ def signout
DfESignInUser.end_session!(session)
redirect_to root_path
end
+
+ private
+
+ def after_sign_in_path
+ if current_user.memberships.count > 1
+ public_send("#{current_service}_organisations_path")
+ else
+ root_path
+ end
+ end
end
diff --git a/app/models/membership.rb b/app/models/membership.rb
new file mode 100644
index 0000000000..e158f1767f
--- /dev/null
+++ b/app/models/membership.rb
@@ -0,0 +1,24 @@
+# == Schema Information
+#
+# Table name: memberships
+#
+# id :uuid not null, primary key
+# organisation_type :string not null
+# created_at :datetime not null
+# updated_at :datetime not null
+# organisation_id :uuid not null
+# user_id :uuid not null
+#
+# Indexes
+#
+# index_memberships_on_organisation (organisation_type,organisation_id)
+# index_memberships_on_user_id (user_id)
+#
+# Foreign Keys
+#
+# fk_rails_... (user_id => users.id)
+#
+class Membership < ApplicationRecord
+ belongs_to :user
+ belongs_to :organisation, polymorphic: true
+end
diff --git a/app/models/provider.rb b/app/models/provider.rb
index 04dc0294e9..0d3eeb9f6e 100644
--- a/app/models/provider.rb
+++ b/app/models/provider.rb
@@ -12,6 +12,8 @@
# index_providers_on_provider_code (provider_code) UNIQUE
#
class Provider < ApplicationRecord
+ has_many :memberships, as: :organisation
+
validates :provider_code, presence: true
validates :provider_code, uniqueness: { case_sensitive: false }
end
diff --git a/app/models/school.rb b/app/models/school.rb
index 903d22acf6..bb72f2c502 100644
--- a/app/models/school.rb
+++ b/app/models/school.rb
@@ -17,6 +17,8 @@
#
class School < ApplicationRecord
belongs_to :gias_school, foreign_key: :urn, primary_key: :urn
+ has_many :memberships, as: :organisation
+
validates :urn, presence: true
validates :urn, uniqueness: { case_sensitive: false }
diff --git a/app/models/user.rb b/app/models/user.rb
index c6e90b221a..6f498e3b14 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -16,6 +16,10 @@
# index_users_on_service_and_email (service,email) UNIQUE
#
class User < ApplicationRecord
+ has_many :memberships
+ has_many :schools, through: :memberships, source: :organisation, source_type: "School"
+ has_many :providers, through: :memberships, source: :organisation, source_type: "Provider"
+
enum :service,
{ no_service: "no_service", claims: "claims", placements: "placements" },
validate: true
diff --git a/app/views/claims/organisations/index.html.erb b/app/views/claims/organisations/index.html.erb
new file mode 100644
index 0000000000..3ac29cb1f8
--- /dev/null
+++ b/app/views/claims/organisations/index.html.erb
@@ -0,0 +1,15 @@
+
+
+
<%= t("organisations") %>
+
+ <% if @schools.any? %>
+
+ <% @schools.each do |school| %>
+ -
+ <%= govuk_link_to school.name, request.env["PATH_INFO"], class: "govuk-link--no-visited-state" %>
+
+ <% end %>
+
+ <% end %>
+
+
diff --git a/app/views/placements/organisations/index.html.erb b/app/views/placements/organisations/index.html.erb
new file mode 100644
index 0000000000..3d0779e1e8
--- /dev/null
+++ b/app/views/placements/organisations/index.html.erb
@@ -0,0 +1,27 @@
+
+
+
<%= t("organisations") %>
+
+ <% if @schools.any? %>
+
<%= t("schools") %>
+
+ <% @schools.each do |school| %>
+ -
+ <%= govuk_link_to school.name, request.env["PATH_INFO"], class: "govuk-link--no-visited-state" %>
+
+ <% end %>
+
+ <% end %>
+
+ <% if @providers.any? %>
+
<%= t("providers") %>
+
+ <% @providers.each do |provider| %>
+ -
+ <%= govuk_link_to provider.provider_code, request.env["PATH_INFO"], class: "govuk-link--no-visited-state" %>
+
+ <% end %>
+
+ <% end %>
+
+
diff --git a/config/initializers/school.rb b/config/initializers/school.rb
new file mode 100644
index 0000000000..b991821207
--- /dev/null
+++ b/config/initializers/school.rb
@@ -0,0 +1,6 @@
+SCHOOLS = [
+ { claims: true },
+ { placements: true },
+ { claims: true, placements: true },
+ { claims: true, placements: true }
+].freeze
diff --git a/config/routes/claims.rb b/config/routes/claims.rb
index dda7a7692a..13281ec609 100644
--- a/config/routes/claims.rb
+++ b/config/routes/claims.rb
@@ -1,3 +1,5 @@
scope module: :claims, as: :claims, constraints: { host: ENV["CLAIMS_HOST"] } do
root to: "pages#index"
+
+ resources :organisations, only: [:index]
end
diff --git a/config/routes/placements.rb b/config/routes/placements.rb
index 5ac869471e..55c69dddc1 100644
--- a/config/routes/placements.rb
+++ b/config/routes/placements.rb
@@ -9,4 +9,6 @@
root to: redirect("/support/organisations")
resources :organisations, only: :index
end
+
+ resources :organisations, only: [:index]
end
diff --git a/db/migrate/20231220143008_create_memberships.rb b/db/migrate/20231220143008_create_memberships.rb
new file mode 100644
index 0000000000..a7252de9e4
--- /dev/null
+++ b/db/migrate/20231220143008_create_memberships.rb
@@ -0,0 +1,10 @@
+class CreateMemberships < ActiveRecord::Migration[7.1]
+ def change
+ create_table :memberships, id: :uuid do |t|
+ t.references :user, null: false, foreign_key: true, type: :uuid
+ t.references :organisation, polymorphic: true, null: false, type: :uuid
+
+ t.timestamps
+ end
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 1d78e06008..f6c7890f7a 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema[7.1].define(version: 2023_12_16_102842) do
+ActiveRecord::Schema[7.1].define(version: 2023_12_20_143008) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@@ -41,6 +41,16 @@
t.index ["urn"], name: "index_gias_schools_on_urn", unique: true
end
+ create_table "memberships", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
+ t.uuid "user_id", null: false
+ t.string "organisation_type", null: false
+ t.uuid "organisation_id", null: false
+ t.datetime "created_at", null: false
+ t.datetime "updated_at", null: false
+ t.index ["organisation_type", "organisation_id"], name: "index_memberships_on_organisation"
+ t.index ["user_id"], name: "index_memberships_on_user_id"
+ end
+
create_table "providers", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
t.string "provider_code", null: false
t.datetime "created_at", null: false
@@ -70,4 +80,5 @@
t.index ["service", "email"], name: "index_users_on_service_and_email", unique: true
end
+ add_foreign_key "memberships", "users"
end
diff --git a/db/seeds.rb b/db/seeds.rb
index 866bc4b315..a2bb65a5d6 100644
--- a/db/seeds.rb
+++ b/db/seeds.rb
@@ -12,3 +12,27 @@
end
Rails.logger.debug "Personas successfully created!"
+
+Rake::Task['gias_update'].invoke unless GiasSchool.any?
+gias_scools = GiasSchool.last(SCHOOLS.count)
+
+SCHOOLS.each.with_index do |school_attributes, index|
+ school = School.find_or_initialize_by(**school_attributes)
+ school.urn = gias_scools[index].urn
+
+ school.save!
+end
+
+User.where(first_name: ["Anne", "Patricia"]).each do |user|
+ school = School.public_send(user.service).first
+ user.memberships.find_or_create_by!(organisation: school)
+end
+
+User.where(first_name: ["Mary", "Colin"]).each do |user|
+ schools = School.where("#{user.service}": true)
+
+ schools.each do |school|
+ user.memberships.find_or_create_by!(organisation: school)
+ end
+end
+
diff --git a/spec/factories/memberships.rb b/spec/factories/memberships.rb
new file mode 100644
index 0000000000..03e2fead86
--- /dev/null
+++ b/spec/factories/memberships.rb
@@ -0,0 +1,26 @@
+# == Schema Information
+#
+# Table name: memberships
+#
+# id :uuid not null, primary key
+# organisation_type :string not null
+# created_at :datetime not null
+# updated_at :datetime not null
+# organisation_id :uuid not null
+# user_id :uuid not null
+#
+# Indexes
+#
+# index_memberships_on_organisation (organisation_type,organisation_id)
+# index_memberships_on_user_id (user_id)
+#
+# Foreign Keys
+#
+# fk_rails_... (user_id => users.id)
+#
+FactoryBot.define do
+ factory :membership do
+ association :user
+ association :organisation, factory: :school
+ end
+end
diff --git a/spec/models/membership_spec.rb b/spec/models/membership_spec.rb
new file mode 100644
index 0000000000..865f0bb0e2
--- /dev/null
+++ b/spec/models/membership_spec.rb
@@ -0,0 +1,32 @@
+# == Schema Information
+#
+# Table name: memberships
+#
+# id :uuid not null, primary key
+# organisation_type :string not null
+# created_at :datetime not null
+# updated_at :datetime not null
+# organisation_id :uuid not null
+# user_id :uuid not null
+#
+# Indexes
+#
+# index_memberships_on_organisation (organisation_type,organisation_id)
+# index_memberships_on_user_id (user_id)
+#
+# Foreign Keys
+#
+# fk_rails_... (user_id => users.id)
+#
+require 'rails_helper'
+
+RSpec.describe Membership, type: :model do
+ subject { create(:membership) }
+
+ context "associations" do
+ it do
+ should belong_to(:user)
+ should belong_to(:organisation)
+ end
+ end
+end
diff --git a/spec/models/provider_spec.rb b/spec/models/provider_spec.rb
index 6f591ecb12..845905534d 100644
--- a/spec/models/provider_spec.rb
+++ b/spec/models/provider_spec.rb
@@ -14,6 +14,12 @@
require "rails_helper"
RSpec.describe Provider, type: :model do
+ context "associations" do
+ it do
+ should have_many(:memberships)
+ end
+ end
+
context "validations" do
subject { create(:provider) }
diff --git a/spec/models/school_spec.rb b/spec/models/school_spec.rb
index 5fe6ff44cd..b96e28db4f 100644
--- a/spec/models/school_spec.rb
+++ b/spec/models/school_spec.rb
@@ -23,6 +23,7 @@
should belong_to(:gias_school).with_foreign_key(:urn).with_primary_key(
:urn
)
+ should have_many(:memberships)
end
end
@@ -33,6 +34,12 @@
it { is_expected.to validate_uniqueness_of(:urn).case_insensitive }
end
+ context "delegations" do
+ it do
+ should delegate_method(:name).to(:gias_school)
+ end
+ end
+
context "scopes" do
describe "services" do
let!(:placements_on) { create(:school, placements: true) }
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index d0933156c4..d9192615a8 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -20,6 +20,14 @@
RSpec.describe User, type: :model do
subject { create(:user) }
+ context "associations" do
+ it do
+ should have_many(:memberships)
+ should have_many(:schools).through(:memberships).source(:organisation)
+ should have_many(:providers).through(:memberships).source(:organisation)
+ end
+ end
+
describe "validations" do
it { is_expected.to validate_presence_of(:email) }
it do
diff --git a/spec/system/claims/view_organisations_spec.rb b/spec/system/claims/view_organisations_spec.rb
new file mode 100644
index 0000000000..c0e40f2c96
--- /dev/null
+++ b/spec/system/claims/view_organisations_spec.rb
@@ -0,0 +1,44 @@
+require "rails_helper"
+
+feature "View organisations as a multi org user" do
+ around do |example|
+ Capybara.app_host = "https://#{ENV["CLAIMS_HOST"]}"
+ example.run
+ Capybara.app_host = nil
+ end
+
+ scenario "I sign in as persona Marry" do
+ persona = given_there_is_an_existing_claims_persona_for("Mary")
+ and_persona_has_multiple_organisations(persona)
+ when_i_visit_the_claims_personas_page
+ when_i_click_sign_in_as(persona)
+ i_can_see_a_list_marys_organisations(persona)
+ end
+end
+
+private
+
+def given_there_is_an_existing_claims_persona_for(persona_name)
+ create(:persona, persona_name.downcase.to_sym, service: "claims")
+end
+
+def and_persona_has_multiple_organisations(persona)
+ school1 = create(:school)
+ school2 = create(:school)
+ create(:membership, user: persona, organisation: school1)
+ create(:membership, user: persona, organisation: school2)
+end
+
+def when_i_visit_the_claims_personas_page
+ visit personas_path
+end
+
+def when_i_click_sign_in_as(persona)
+ click_on "Sign In as #{persona.first_name}"
+end
+
+def i_can_see_a_list_marys_organisations(persona)
+ expect(page).to have_content("Organisations")
+ expect(page).to have_content(persona.schools.first.name)
+ expect(page).to have_content(persona.schools.last.name)
+end
diff --git a/spec/system/placements/view_organisations_spec.rb b/spec/system/placements/view_organisations_spec.rb
new file mode 100644
index 0000000000..20ba3f2e1d
--- /dev/null
+++ b/spec/system/placements/view_organisations_spec.rb
@@ -0,0 +1,46 @@
+require "rails_helper"
+
+feature "View organisations as a multi org user" do
+ around do |example|
+ Capybara.app_host = "https://#{ENV["PLACEMENTS_HOST"]}"
+ example.run
+ Capybara.app_host = nil
+ end
+
+ scenario "I sign in as persona Marry" do
+ persona = given_there_is_an_existing_placements_persona_for("Mary")
+ and_persona_has_multiple_organisations(persona)
+ when_i_visit_the_placements_personas_page
+ when_i_click_sign_in_as(persona)
+ i_can_see_a_list_marys_organisations(persona)
+ end
+end
+
+private
+
+def given_there_is_an_existing_placements_persona_for(persona_name)
+ create(:persona, persona_name.downcase.to_sym, service: "placements")
+end
+
+def and_persona_has_multiple_organisations(persona)
+ school1 = create(:school)
+ provider = create(:provider)
+ create(:membership, user: persona, organisation: school1)
+ create(:membership, user: persona, organisation: provider)
+end
+
+def when_i_visit_the_placements_personas_page
+ visit personas_path
+end
+
+def when_i_click_sign_in_as(persona)
+ click_on "Sign In as #{persona.first_name}"
+end
+
+def i_can_see_a_list_marys_organisations(persona)
+ expect(page).to have_content("Organisations")
+ expect(page).to have_content("Schools")
+ expect(page).to have_content("Providers")
+ expect(page).to have_content(persona.schools.first.name)
+ expect(page).to have_content(persona.providers.first.provider_code)
+end