From 513ac26cc3354a82c5b40a6a3a41718a5f4875bd Mon Sep 17 00:00:00 2001 From: Richard Worrall Date: Mon, 27 Jan 2025 14:48:20 +0000 Subject: [PATCH] [CPDNPQ-2435] Add "in review" applications screen (#2142) * [CPDNPQ-2435] Add "in review" applications screen * [CPDNPQ-2435] Add missing review employment_type * [CPDNPQ-2435] Add seeds for applications in review --- .../admin_navigation_structure.rb | 17 ++- .../admin/applications/reviews_controller.rb | 54 ++++++++ .../admin_service/applications_search.rb | 4 +- .../admin/applications/reviews/index.html.erb | 79 +++++++++++ config/routes.rb | 3 + db/seeds/base/add_applications.rb | 13 ++ spec/factories/schools.rb | 2 +- .../admin/applications_in_review_spec.rb | 131 ++++++++++++++++++ .../admin_service/applications_search_spec.rb | 39 ++++-- 9 files changed, 326 insertions(+), 16 deletions(-) create mode 100644 app/controllers/npq_separation/admin/applications/reviews_controller.rb create mode 100644 app/views/npq_separation/admin/applications/reviews/index.html.erb create mode 100644 spec/features/npq_separation/admin/applications_in_review_spec.rb diff --git a/app/components/npq_separation/navigation_structures/admin_navigation_structure.rb b/app/components/npq_separation/navigation_structures/admin_navigation_structure.rb index 9dced35213..8910d2a462 100644 --- a/app/components/npq_separation/navigation_structures/admin_navigation_structure.rb +++ b/app/components/npq_separation/navigation_structures/admin_navigation_structure.rb @@ -19,7 +19,7 @@ def structure name: "Applications", href: npq_separation_admin_applications_path, prefix: "/npq-separation/admin/applications", - ) => [], + ) => application_nodes, Node.new( name: "Cohorts", href: npq_separation_admin_cohorts_path, @@ -75,6 +75,21 @@ def structure } end + def application_nodes + [ + Node.new( + name: "All applications", + href: npq_separation_admin_applications_path, + prefix: /\/npq-separation\/admin\/applications(?!\/reviews)$/, + ), + Node.new( + name: "In review", + href: npq_separation_admin_application_reviews_path, + prefix: "/npq-separation/admin/applications/reviews", + ), + ] + end + def cohort_nodes [ Node.new( diff --git a/app/controllers/npq_separation/admin/applications/reviews_controller.rb b/app/controllers/npq_separation/admin/applications/reviews_controller.rb new file mode 100644 index 0000000000..f10789625a --- /dev/null +++ b/app/controllers/npq_separation/admin/applications/reviews_controller.rb @@ -0,0 +1,54 @@ +# frozen_string_literal: true + +module NpqSeparation + module Admin + module Applications + class ReviewsController < NpqSeparation::AdminController + helper_method :employment_types + + def index + applications = Application.includes(:private_childcare_provider, :school, :user) + .merge(review_scope) + .merge(filter_scope) + .merge(search_scope) + + @pagy, @applications = pagy(applications, limit: 9) + end + + private + + def employment_types + %w[ + hospital_school + lead_mentor_for_accredited_itt_provider + local_authority_supply_teacher + local_authority_virtual_school + young_offender_institution + other + ] + end + + def filter_params + params.permit %i[ + employment_type + referred_by_return_to_teaching_adviser + cohort_id + ] + end + + def review_scope + Application.where(employment_type: employment_types) + .or(Application.where(referred_by_return_to_teaching_adviser: "yes")) + end + + def filter_scope + Application.where(filter_params.compact_blank) + end + + def search_scope + AdminService::ApplicationsSearch.new(q: params[:q]).call + end + end + end + end +end diff --git a/app/services/admin_service/applications_search.rb b/app/services/admin_service/applications_search.rb index 9df613b8e6..ffd587013c 100644 --- a/app/services/admin_service/applications_search.rb +++ b/app/services/admin_service/applications_search.rb @@ -10,6 +10,8 @@ def call if q.present? chain = chain.where("users.email ilike ?", "%#{q}%") + chain = chain.or(default_scope.where("users.full_name ilike ?", "%#{q}%")) + chain = chain.or(default_scope.where("applications.employer_name ilike ? OR schools.name ilike ?", "%#{q}%", "%#{q}%")) chain = chain.or(default_scope.where(ecf_id: q)) chain = chain.or(default_scope.where(users: { ecf_id: q })) end @@ -20,6 +22,6 @@ def call private def default_scope - Application.joins(:user).includes(:user, :course, :lead_provider).order(id: :asc) + Application.joins(:school, :user).includes(:course, :lead_provider, :school, :user).order(id: :asc) end end diff --git a/app/views/npq_separation/admin/applications/reviews/index.html.erb b/app/views/npq_separation/admin/applications/reviews/index.html.erb new file mode 100644 index 0000000000..a6cb0b630d --- /dev/null +++ b/app/views/npq_separation/admin/applications/reviews/index.html.erb @@ -0,0 +1,79 @@ +

Registrations in review

+ + + +<%= + govuk_table do |table| + table.with_head do |header| + header.with_row do |row| + row.with_cell(text: "Participant") + row.with_cell(text: "Eligible for funding") + row.with_cell(text: "Provider approval status") + row.with_cell(text: "Notes", classes: 'govuk-!-width-one-third') + row.with_cell(text: "Registration submitted date") + end + end + + table.with_body do |body| + @applications.each do |application| + body.with_row do |row| + row.with_cell do + concat govuk_link_to(application.user.full_name, npq_separation_admin_user_path(application.user)) + concat tag.p(application.employment_type.try(:humanize), class: 'govuk-body-s govuk-!-margin-top-1 govuk-!-margin-bottom-1') + concat tag.p(application.employer_name_to_display, class: 'govuk-body-s govuk-!-margin-top-1 govuk-!-margin-bottom-1') + end + row.with_cell(text: boolean_red_green_tag(application.eligible_for_funding)) + row.with_cell(text: application.lead_provider_approval_status.humanize) + row.with_cell(text: application.notes) + row.with_cell(text: application.created_at.to_formatted_s(:govuk_short)) + end + end + end + end +%> + +<%= govuk_pagination(pagy: @pagy) %> diff --git a/config/routes.rb b/config/routes.rb index 1c00a8007d..c327af9b88 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -220,6 +220,9 @@ end resources :applications, only: %i[index show] do + collection do + resources :reviews, controller: "applications/reviews", as: "application_reviews", only: %i[index] + end member do namespace :applications, path: nil do resource :revert_to_pending, controller: "revert_to_pending", only: %i[new create] diff --git a/db/seeds/base/add_applications.rb b/db/seeds/base/add_applications.rb index 8784018cb2..d06be8c668 100644 --- a/db/seeds/base/add_applications.rb +++ b/db/seeds/base/add_applications.rb @@ -136,3 +136,16 @@ ) end end + +# Make sure some applications will appear in the In Review list +[ + { employment_type: "hospital_school" }, + { employment_type: "lead_mentor_for_accredited_itt_provider" }, + { employment_type: "local_authority_supply_teacher" }, + { employment_type: "local_authority_virtual_school" }, + { employment_type: "young_offender_institution" }, + { employment_type: "other" }, + { referred_by_return_to_teaching_adviser: "yes" }, +].each do |application_attrs| + FactoryBot.create(:application, **application_attrs) +end diff --git a/spec/factories/schools.rb b/spec/factories/schools.rb index 28f21aadcb..098fae9230 100644 --- a/spec/factories/schools.rb +++ b/spec/factories/schools.rb @@ -1,6 +1,6 @@ FactoryBot.define do factory :school do - sequence(:name) { |n| "school #{n}" } + sequence(:name) { Faker::Educator.primary_school } sequence(:urn) { rand(100_000..999_999).to_s } sequence(:ukprn) { rand(10_000_000..99_999_999).to_s } establishment_status_code { %w[1 3 4].sample } diff --git a/spec/features/npq_separation/admin/applications_in_review_spec.rb b/spec/features/npq_separation/admin/applications_in_review_spec.rb new file mode 100644 index 0000000000..2f71b7c358 --- /dev/null +++ b/spec/features/npq_separation/admin/applications_in_review_spec.rb @@ -0,0 +1,131 @@ +require "rails_helper" + +RSpec.feature "Applications in review", type: :feature do + include Helpers::AdminLogin + + let(:cohort_21) { create(:cohort, start_year: 2021) } + let(:cohort_22) { create(:cohort, start_year: 2022) } + + let!(:normal_application) { create(:application) } + let!(:application_for_hospital_school) { create(:application, employment_type: "hospital_school", employer_name: Faker::Company.name, cohort: cohort_21, referred_by_return_to_teaching_adviser: "yes") } + let!(:application_for_la_supply_teacher) { create(:application, employment_type: "local_authority_supply_teacher", cohort: cohort_22, referred_by_return_to_teaching_adviser: "no") } + let!(:application_for_la_virtual_school) { create(:application, employment_type: "local_authority_virtual_school") } + let!(:application_for_lead_mentor) { create(:application, employment_type: "local_authority_virtual_school") } + let!(:application_for_young_offender_institution) { create(:application, employment_type: "young_offender_institution") } + let!(:application_for_other) { create(:application, employment_type: "other") } + let!(:application_for_rtta_yes) { create(:application, referred_by_return_to_teaching_adviser: "yes") } + let!(:application_for_rtta_no) { create(:application, referred_by_return_to_teaching_adviser: "no") } + + before do + sign_in_as create(:admin) + visit npq_separation_admin_applications_path + click_on "In review" + end + + scenario "listing" do + rows = [ + application_for_hospital_school, + application_for_la_supply_teacher, + application_for_la_virtual_school, + application_for_lead_mentor, + application_for_young_offender_institution, + application_for_other, + application_for_rtta_yes, + ].map do |application| + [ + [application.user.full_name, application.employment_type.try(:humanize), application.employer_name].compact.join, + application.eligible_for_funding ? "Yes" : "No", + application.lead_provider_approval_status.humanize, + application.notes.to_s, + application.created_at.to_fs(:govuk_short), + ] + end + + expect(page).to have_table(rows:) + expect(page).not_to have_text normal_application.user.full_name + expect(page).not_to have_text application_for_rtta_no.user.full_name + end + + scenario "searching with participant ID" do + fill_in("Enter the participant ID", with: application_for_hospital_school.user.ecf_id) + click_on "Search" + + expect(page).to have_text(application_for_hospital_school.user.full_name) + expect(page).not_to have_text(application_for_la_supply_teacher.user.full_name) + end + + scenario "searching with participant name" do + fill_in("Enter the participant ID", with: application_for_hospital_school.user.full_name) + click_on "Search" + + expect(page).to have_text(application_for_hospital_school.user.full_name) + expect(page).not_to have_text(application_for_la_supply_teacher.user.full_name) + end + + scenario "searching with participant email" do + fill_in("Enter the participant ID", with: application_for_hospital_school.user.email) + click_on "Search" + + expect(page).to have_text(application_for_hospital_school.user.full_name) + expect(page).not_to have_text(application_for_la_supply_teacher.user.full_name) + end + + scenario "searching with employer name" do + fill_in("Enter the participant ID", with: application_for_hospital_school.employer_name) + click_on "Search" + + expect(page).to have_text(application_for_hospital_school.user.full_name) + expect(page).not_to have_text(application_for_la_supply_teacher.user.full_name) + end + + scenario "searching with application ID" do + fill_in("Enter the participant ID", with: application_for_hospital_school.ecf_id) + click_on "Search" + + expect(page).to have_text(application_for_hospital_school.user.full_name) + expect(page).not_to have_text(application_for_la_supply_teacher.user.full_name) + end + + scenario "filtering by employment type" do + select "Hospital school", from: "Employment type" + click_on "Search" + + expect(page).to have_text(application_for_hospital_school.user.full_name) + expect(page).not_to have_text(application_for_la_supply_teacher.user.full_name) + end + + scenario "filtering by referred by return to teaching adviser" do + application_for_hospital_school.update!(employer_name: "Return to teaching adviser referral") + + select "Yes", from: "Referred by return to teaching adviser" + click_on "Search" + + expect(page).to have_text(application_for_hospital_school.user.full_name) + expect(page).to have_text(application_for_rtta_yes.user.full_name) + expect(page).not_to have_text(application_for_la_supply_teacher.user.full_name) + expect(page).not_to have_text(application_for_la_virtual_school.user.full_name) + expect(page).not_to have_text(application_for_rtta_no.user.full_name) + end + + scenario "filtering by cohort" do + select "2021/22", from: "Cohort" + click_on "Search" + + expect(page).to have_text(application_for_hospital_school.user.full_name) + expect(page).not_to have_text(application_for_la_supply_teacher.user.full_name) + end + + scenario "combining search and filters" do + fill_in("Enter the participant ID", with: application_for_hospital_school.user.full_name) + select "2021/22", from: "Cohort" + click_on "Search" + + expect(page).to have_text(application_for_hospital_school.user.full_name) + expect(page).not_to have_text(application_for_la_supply_teacher.user.full_name) + + select "2022/23", from: "Cohort" + click_on "Search" + expect(page).not_to have_text(application_for_hospital_school.user.full_name) + expect(page).not_to have_text(application_for_la_supply_teacher.user.full_name) + end +end diff --git a/spec/lib/services/admin_service/applications_search_spec.rb b/spec/lib/services/admin_service/applications_search_spec.rb index 2dced29dc9..96947646fa 100644 --- a/spec/lib/services/admin_service/applications_search_spec.rb +++ b/spec/lib/services/admin_service/applications_search_spec.rb @@ -1,34 +1,47 @@ require "rails_helper" RSpec.describe AdminService::ApplicationsSearch do - subject { described_class.new(q:) } - - let!(:application) { create(:application) } + let(:service) { described_class.new(q:) } + let!(:application) { create(:application, employer_name: Faker::Company.name) } let!(:user) { application.user } describe "#call" do - context "when partial email match" do + subject { service.call } + + context "when email partially matches" do let(:q) { user.email.split("@").first } - it "returns the hit" do - expect(subject.call).to include(application) - end + it { is_expected.to include(application) } + end + + context "when name partially matches" do + let(:q) { user.full_name.split(" ").first } + + it { is_expected.to include(application) } + end + + context "when employer_name matches" do + let(:q) { application.employer_name.split(" ").first } + + it { is_expected.to include(application) } + end + + context "when school name matches" do + let(:q) { application.school.name.split(" ").first } + + it { is_expected.to include(application) } end context "when application#ecf_id match" do let(:q) { application.ecf_id } - it "returns the hit" do - expect(subject.call).to include(application) - end + it { is_expected.to include(application) } end context "when user#ecf_id match" do let(:q) { user.ecf_id } - it "returns the hit" do - expect(subject.call).to include(application) - end + it { is_expected.to include(application) } end end end