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? %> + + <% 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") %>

+ + <% end %> + + <% if @providers.any? %> +

<%= t("providers") %>

+ + <% 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