diff --git a/app/controllers/concerns/study_mode_vacancy_mapper.rb b/app/controllers/concerns/study_mode_vacancy_mapper.rb deleted file mode 100644 index 45724e2986..0000000000 --- a/app/controllers/concerns/study_mode_vacancy_mapper.rb +++ /dev/null @@ -1,18 +0,0 @@ -# frozen_string_literal: true - -module StudyModeVacancyMapper - extend ActiveSupport::Concern - - def update_vac_status(study_mode, site_status) - case study_mode - when 'full_time' - site_status.update(vac_status: :full_time_vacancies) - when 'part_time' - site_status.update(vac_status: :part_time_vacancies) - when 'full_time_or_part_time' - site_status.update(vac_status: :both_full_time_and_part_time_vacancies) - else - raise "Unexpected study mode #{study_mode}" - end - end -end diff --git a/app/controllers/publish/courses/outcome_controller.rb b/app/controllers/publish/courses/outcome_controller.rb index 7f57f4ecac..fa603ddd20 100644 --- a/app/controllers/publish/courses/outcome_controller.rb +++ b/app/controllers/publish/courses/outcome_controller.rb @@ -21,11 +21,20 @@ def new end def edit + authorize course, :can_update_qualification? + super + rescue Pundit::NotAuthorizedError + redirect_to publish_provider_recruitment_cycle_course_path( + course.provider.provider_code, + course.provider.recruitment_cycle_year, + course.course_code + ) end def update authorize(provider) + authorize course, :can_update_qualification? @errors = errors return render :edit if @errors.present? @@ -52,8 +61,13 @@ def errors def handle_qualification_update if undergraduate_to_other_qualification? - @course.enrichments.find_or_initialize_draft.update(course_length: nil, salary_details: nil) + @course.update( + a_level_subject_requirements: [], + accept_pending_a_level: nil, + accept_a_level_equivalency: nil, + additional_a_level_equivalencies: nil + ) redirect_to funding_type_publish_provider_recruitment_cycle_course_path( @course.provider_code, @@ -64,7 +78,10 @@ def handle_qualification_update else if undergraduate_degree_with_qts? Publish::Courses::AssignTdaAttributesService.new(@course).call + @course.save + + @course.ensure_site_statuses_match_full_time end redirect_to details_publish_provider_recruitment_cycle_course_path( diff --git a/app/models/concerns/courses/edit_options/qualification_concern.rb b/app/models/concerns/courses/edit_options/qualification_concern.rb index 0d4fcd7e5c..2c85571ae8 100644 --- a/app/models/concerns/courses/edit_options/qualification_concern.rb +++ b/app/models/concerns/courses/edit_options/qualification_concern.rb @@ -16,9 +16,13 @@ def qualification_options def qualifications_with_qts qts_list = Course.qualifications.keys.grep(/qts/) - return qts_list if tda_active? + qts_list -= %w[undergraduate_degree_with_qts] if !tda_active? || non_tda_published? - qts_list - %w[undergraduate_degree_with_qts] + qts_list + end + + def non_tda_published? + !teacher_degree_apprenticeship? && is_published? end def qualifications_without_qts diff --git a/app/models/course.rb b/app/models/course.rb index c7d929efba..2e57d48e50 100644 --- a/app/models/course.rb +++ b/app/models/course.rb @@ -6,7 +6,6 @@ class Course < ApplicationRecord include ChangedAt include TouchProvider include Courses::EditOptions - include StudyModeVacancyMapper include TimeFormat after_initialize :set_defaults @@ -811,8 +810,27 @@ def find_a_level_subject_requirement!(uuid) requirement.with_indifferent_access end + def ensure_site_statuses_match_full_time + site_statuses.each do |site_status| + update_vac_status('full_time', site_status) + end + end + private + def update_vac_status(study_mode, site_status) + case study_mode + when 'full_time' + site_status.update(vac_status: :full_time_vacancies) + when 'part_time' + site_status.update(vac_status: :part_time_vacancies) + when 'full_time_or_part_time' + site_status.update(vac_status: :both_full_time_and_part_time_vacancies) + else + raise "Unexpected study mode #{study_mode}" + end + end + def add_site!(site:) is_course_new = ucas_status == :new site_status = site_statuses.find_or_initialize_by(site:) diff --git a/app/policies/course_policy.rb b/app/policies/course_policy.rb index 7954acf106..0448de3fa8 100644 --- a/app/policies/course_policy.rb +++ b/app/policies/course_policy.rb @@ -39,6 +39,10 @@ def can_update_funding_type? course.draft_or_rolled_over? end + def can_update_qualification? + !(course.teacher_degree_apprenticeship? && course.is_published?) && !course.is_withdrawn? + end + alias preview? show? alias apply? show? alias details? show? diff --git a/app/views/publish/courses/_basic_details_tab.html.erb b/app/views/publish/courses/_basic_details_tab.html.erb index 2e43035962..18900b33b3 100644 --- a/app/views/publish/courses/_basic_details_tab.html.erb +++ b/app/views/publish/courses/_basic_details_tab.html.erb @@ -62,13 +62,13 @@ summary_list.with_row(html_attributes: { data: { qa: "course__outcome" } }) do |row| row.with_key { "Qualification" } row.with_value { course.outcome } - if course.is_withdrawn? - row.with_action - else + if policy(course).can_update_qualification? row.with_action( href: outcome_publish_provider_recruitment_cycle_course_path(@provider.provider_code, course.recruitment_cycle_year, course.course_code), visually_hidden_text: "outcome" ) + else + row.with_action end end diff --git a/spec/controllers/publish/courses/outcome_controller_spec.rb b/spec/controllers/publish/courses/outcome_controller_spec.rb new file mode 100644 index 0000000000..bdb9b28df9 --- /dev/null +++ b/spec/controllers/publish/courses/outcome_controller_spec.rb @@ -0,0 +1,127 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe Publish::Courses::OutcomeController do + let(:recruitment_cycle) { create(:recruitment_cycle, year: 2025) } + let(:user) { create(:user, providers: [build(:provider, recruitment_cycle:)]) } + let(:provider) { user.providers.first } + + before do + allow(controller).to receive(:authenticate).and_return(true) + controller.instance_variable_set(:@current_user, user) + allow(Settings.features).to receive(:teacher_degree_apprenticeship).and_return(true) + end + + describe '#edit' do + context 'when teacher degree apprenticeship published course' do + it 'redirects to the course page' do + course = create( + :course, + :resulting_in_undergraduate_degree_with_qts, + :with_teacher_degree_apprenticeship, + :published, + provider:, + study_mode: :part_time, + site_statuses: [build(:site_status, :part_time_vacancies, :findable)] + ) + + get :edit, params: { + provider_code: provider.provider_code, + recruitment_cycle_year: provider.recruitment_cycle_year, + code: course.course_code + } + + expect(response).to redirect_to( + publish_provider_recruitment_cycle_course_path( + provider.provider_code, + provider.recruitment_cycle_year, + course.course_code + ) + ) + end + end + + context 'when teacher degree apprenticeship draft course' do + it 'renders the edit outcome' do + course = create( + :course, + :resulting_in_undergraduate_degree_with_qts, + :with_teacher_degree_apprenticeship, + :draft_enrichment, + provider:, + study_mode: :part_time, + site_statuses: [build(:site_status, :part_time_vacancies, :findable)] + ) + + get :edit, params: { + provider_code: provider.provider_code, + recruitment_cycle_year: provider.recruitment_cycle_year, + code: course.course_code + } + + expect(response).to render_template(:edit) + end + end + end + + describe '#update' do + context 'when changing from a QTS to teacher degree apprenticeship course' do + it 'assigns teacher degree apprenticeship course defaults' do + course = create( + :course, + :resulting_in_qts, + provider:, + study_mode: :part_time, + site_statuses: [build(:site_status, :part_time_vacancies, :findable)] + ) + create(:course_enrichment, :initial_draft, course_length: :TwoYears, course:) + + put :update, params: { + course: { qualification: 'undergraduate_degree_with_qts' }, + provider_code: provider.provider_code, + recruitment_cycle_year: provider.recruitment_cycle_year, + code: course.course_code + } + + course.reload + + expect(course.funding_type).to eq('apprenticeship') + expect(course.can_sponsor_skilled_worker_visa).to be(false) + expect(course.can_sponsor_student_visa).to be(false) + expect(course.additional_degree_subject_requirements).to be(false) + expect(course.degree_subject_requirements).to be_nil + expect(course.degree_grade).to eq('not_required') + expect(course.study_mode).to eq('full_time') + expect(course.site_statuses.map(&:vac_status).uniq.first).to eq('full_time_vacancies') + expect(course.enrichments.max_by(&:created_at).course_length).to eq('4 years') + end + end + + context 'when changing from teacher degree apprenticeship to a QTS course' do + it 'clear teacher degree specific defaults' do + course = create( + :course, + :with_teacher_degree_apprenticeship, + :resulting_in_undergraduate_degree_with_qts, + :with_a_level_requirements, + provider: + ) + + put :update, params: { + course: { qualification: 'qts' }, + provider_code: provider.provider_code, + recruitment_cycle_year: provider.recruitment_cycle_year, + code: course.course_code + } + + course.reload + + expect(course.a_level_subject_requirements).to eq([]) + expect(course.accept_a_level_equivalency).to be_nil + expect(course.accept_pending_a_level).to be_nil + expect(course.additional_a_level_equivalencies).to be_nil + end + end + end +end diff --git a/spec/features/publish/courses/publishing_a_teacher_degree_apprenticeship_course_with_validation_errors_spec.rb b/spec/features/publish/courses/publishing_a_teacher_degree_apprenticeship_course_with_validation_errors_spec.rb index 244e441815..9bcf7245f3 100644 --- a/spec/features/publish/courses/publishing_a_teacher_degree_apprenticeship_course_with_validation_errors_spec.rb +++ b/spec/features/publish/courses/publishing_a_teacher_degree_apprenticeship_course_with_validation_errors_spec.rb @@ -3,9 +3,12 @@ require 'rails_helper' feature 'Publishing courses errors', { can_edit_current_and_next_cycles: false } do - scenario 'The error links target the correct pages' do + before do given_i_am_authenticated_as_a_provider_user and_the_tda_feature_flag_is_active + end + + scenario 'The error links target the correct pages' do and_there_is_an_invalid_tda_course_i_want_to_publish when_i_visit_the_course_page @@ -45,6 +48,22 @@ then_the_course_is_published end + scenario 'when the TDA course is published and I try to change qualification' do + given_there_is_an_published_tda_course + when_i_visit_the_course_page + and_i_enter_the_basic_details_tab + then_there_is_no_change_qualification_link + end + + scenario 'when the non TDA course is published and I try to change qualification' do + given_there_is_an_published_qts_course + when_i_visit_the_course_page + and_i_enter_the_basic_details_tab + and_i_click_change_qualification + then_the_tda_option_is_not_available + and_i_on_the_change_qualification_page + end + def given_i_am_authenticated_as_a_provider_user recruitment_cycle = create(:recruitment_cycle, year: 2025) @user = create(:user, providers: [build(:provider, recruitment_cycle:, provider_type: 'lead_school', sites: [build(:site), build(:site)], study_sites: [build(:site, :study_site), build(:site, :study_site)])]) @@ -82,6 +101,34 @@ def and_there_is_an_invalid_tda_course_i_want_to_publish @course.sites << build_list(:site, 1, provider: @provider) end + def given_there_is_an_published_tda_course + @course = create( + :course, + :with_teacher_degree_apprenticeship, + :resulting_in_undergraduate_degree_with_qts, + :with_gcse_equivalency, + :published, + provider: @provider, + accrediting_provider: @accredited_provider, + a_level_subject_requirements: [], + accept_pending_a_level: nil, + accept_a_level_equivalency: nil + ) + @course.sites << build_list(:site, 1, provider: @provider) + end + + def given_there_is_an_published_qts_course + @course = create( + :course, + :resulting_in_qts, + :with_gcse_equivalency, + :published, + provider: @provider, + accrediting_provider: @accredited_provider + ) + @course.sites << build_list(:site, 1, provider: @provider) + end + def when_i_visit_the_course_page publish_provider_courses_show_page.load( provider_code: @provider.provider_code, @@ -100,6 +147,32 @@ def then_i_am_on_the_course_page ) end + def then_there_is_no_change_qualification_link + expect(publish_provider_courses_details_page.outcome.actions.text).to be_empty + end + + def and_i_enter_the_basic_details_tab + click_on 'Basic details' + end + + def and_i_click_change_qualification + publish_provider_courses_details_page.outcome.actions.find('a').click + end + + def then_the_tda_option_is_not_available + expect(page).to have_no_field('Teacher degree apprenticeship (TDA) with QTS', type: 'radio') + end + + def and_i_on_the_change_qualification_page + expect(page).to have_current_path( + outcome_publish_provider_recruitment_cycle_course_path( + @course.provider.provider_code, + @course.provider.recruitment_cycle_year, + @course.course_code + ) + ) + end + def and_i_click_the_publish_link publish_provider_courses_show_page.course_button_panel.publish_button.click end diff --git a/spec/models/concerns/courses/edit_options/qualification_concern_spec.rb b/spec/models/concerns/courses/edit_options/qualification_concern_spec.rb index 6548fb0aa3..0de0b341fe 100644 --- a/spec/models/concerns/courses/edit_options/qualification_concern_spec.rb +++ b/spec/models/concerns/courses/edit_options/qualification_concern_spec.rb @@ -9,6 +9,10 @@ attr_accessor :level def tda_active?; end + + def teacher_degree_apprenticeship?; end + + def is_published?; end end klass.new diff --git a/spec/models/concerns/courses/edit_options_spec.rb b/spec/models/concerns/courses/edit_options_spec.rb index 8d0a67fe08..e45e97feef 100644 --- a/spec/models/concerns/courses/edit_options_spec.rb +++ b/spec/models/concerns/courses/edit_options_spec.rb @@ -75,6 +75,9 @@ end describe 'qualifications' do + let(:provider) { create(:provider, recruitment_cycle: create(:recruitment_cycle, year: 2025)) } + let(:course) { create(:course, provider:, level: 'primary', subjects: [subjects]) } + context "for a course that's not further education" do it 'returns only QTS options for users to choose between' do expect(course.qualification_options).to eq(%w[qts pgce_with_qts pgde_with_qts]) @@ -95,6 +98,38 @@ end end end + + context 'when TDA is active' do + before do + allow(Settings.features).to receive(:teacher_degree_apprenticeship).and_return(true) + end + + it 'includes undergraduate_degree_with_qts' do + expect(course.qualification_options).to include('undergraduate_degree_with_qts') + end + end + + context 'when TDA is not active' do + before do + allow(Settings.features).to receive(:teacher_degree_apprenticeship).and_return(false) + end + + it 'does not include undergraduate_degree_with_qts' do + expect(course.qualification_options).not_to include('undergraduate_degree_with_qts') + end + end + + context 'when the course is non-TDA and published' do + let(:course) { create(:course, :resulting_in_qts, :published) } + + before do + allow(Settings.features).to receive(:teacher_degree_apprenticeship).and_return(true) + end + + it 'does not include undergraduate_degree_with_qts' do + expect(course.qualification_options).not_to include('undergraduate_degree_with_qts') + end + end end describe 'age_range' do diff --git a/spec/models/course_spec.rb b/spec/models/course_spec.rb index 830c89d259..d171c87b8e 100644 --- a/spec/models/course_spec.rb +++ b/spec/models/course_spec.rb @@ -3018,6 +3018,22 @@ end end + describe '#ensure_site_statuses_match_full_time' do + it 'updates all site statuses to full_time_vacancies' do + course = create(:course, study_mode: :part_time) + site_status_part_time = create(:site_status, course:, vac_status: :part_time_vacancies) + + course.study_mode = 'full_time' + course.ensure_site_statuses_match_full_time + + course.reload + site_status_part_time.reload + + expect(site_status_part_time.vac_status).to eq('full_time_vacancies') + expect(course.site_statuses.map(&:vac_status).first).to eq('full_time_vacancies') + end + end + describe 'funding_type and program_type' do context 'setting the funding_type to apprenticeship' do it 'sets the funding_type to apprenticeship and program_type to pg_teaching_apprenticeship' do diff --git a/spec/policies/course_policy_spec.rb b/spec/policies/course_policy_spec.rb index 17a3d14b64..0cbb20a2af 100644 --- a/spec/policies/course_policy_spec.rb +++ b/spec/policies/course_policy_spec.rb @@ -46,6 +46,73 @@ end end + permissions :can_update_qualification? do + context 'when the course is a TDA and is published' do + let(:course) do + create( + :course, + :with_teacher_degree_apprenticeship, + :resulting_in_undergraduate_degree_with_qts, + :with_gcse_equivalency, + :published + ) + end + + it { is_expected.not_to permit(user, course) } + end + + context 'when the course is a TDA but not published' do + let(:course) do + create( + :course, + :with_teacher_degree_apprenticeship, + :resulting_in_undergraduate_degree_with_qts, + :with_gcse_equivalency + ) + end + + it { is_expected.to permit(user, course) } + end + + context 'when the course is not a TDA but is published' do + let(:course) do + create( + :course, + :resulting_in_qts, + :with_gcse_equivalency, + :published + ) + end + + it { is_expected.to permit(user, course) } + end + + context 'when the course is withdrawn' do + let(:course) do + create( + :course, + :resulting_in_qts, + :with_gcse_equivalency, + :withdrawn + ) + end + + it { is_expected.not_to permit(user, course) } + end + + context 'when the course is not a TDA, not published, and not withdrawn' do + let(:course) do + create( + :course, + :resulting_in_qts, + :with_gcse_equivalency + ) + end + + it { is_expected.to permit(user, course) } + end + end + describe '#permitted_attributes' do subject { described_class.new(user, build(:course)) }