Repository: DFE-Digital/apply-for-teacher-training
Pull request #10449: Find candidates additional filters
Author: login: CatalinVoineag
Labels: name: deploy_v2
description: Deploy the review app to AKS
Created: 2025-03-04T15:23:34Z | Merged: 2025-03-07T08:57:13Z

## Context

This adds more filters to the find candidates index page.
- Subjects
- Study type (full time, part time)
- Course type (undergraduate, postgraduate). We treat this as tda or not :) 

With the subjects filters and course type, we need to pass multiple values to the backend from 1 checkbox. This is because 1 checkbox can mean multiple ids, Like we have multiple subjects with the same name, so instead of showing multiple subjects with the same name we group the ids behind one checkbox.

This is now how checkboxes should be used so I have to do some string splitting, watch the video to show how we pass the ids from a checkbox

Same happens for course_types, if they user filters by postgradute, we need to pass all the program types of a course, excluding TDA.

The second commit of this PR includes a fix to the checkbox search filter, changing the display to flex fixes this issues
![Screenshot from 2025-03-06 10-04-43](https://github.com/user-attachments/assets/33483b00-9e40-4052-9c02-abb1e4ecf837)



## Guidance to review

Go onto review and play with the filters. Log on as a provider with `dev-provider`



https://github.com/user-attachments/assets/dba2ae65-4ffc-42ed-80bf-34b8dc90b638






## Things to check

- [ ] If the code removes any existing feature flags, a data migration has also been added to delete the entry from the database
- [ ] This code does not rely on migrations in the same Pull Request
- [ ] If this code includes a migration adding or changing columns, it also backfills existing records for consistency
- [ ] If this code adds a column to the DB, decide whether it needs to be in analytics yml file or analytics blocklist, if included inform data insights team of the changes
- [ ] If this code adds a column that may include PII, the sanitise.sql script and 0025-protecting-personal-data-in-production-dump.md ADR have been updated
- [ ] API release notes have been updated if necessary
- [ ] If it adds a significant user-facing change, is it documented in the [CHANGELOG](CHANGELOG.md)?
- [x] Attach the PR to the Trello card



Diff:
diff --git a/app/components/utility/filter_component.html.erb b/app/components/utility/filter_component.html.erb
index ab67946d5d2..24a48691deb 100644
--- a/app/components/utility/filter_component.html.erb
+++ b/app/components/utility/filter_component.html.erb
@@ -67,7 +67,7 @@
                     <% end %>
                      <% elsif filter[:type] == :checkbox_filter %>
                     <div id="<%= filter[:name] %>" class="app-checkbox-filter app-checkbox-filter--enhanced">
-                      <% if active_filters&.find { |f| f[:name] == filter[:name] } %>
+                      <% if active_filters&.find { |f| f[:name] == filter[:name] } && filter[:hide_tags].blank? %>
                         <div class="app-checkbox-filter__selected">
                           <ul class="app-checkbox-filter__tags">
                             <% tags_for_active_filter(filter).each do |tag| %>
diff --git a/app/controllers/provider_interface/candidate_pool/candidates_controller.rb b/app/controllers/provider_interface/candidate_pool/candidates_controller.rb
index 46bbc2eb80a..6ceec61c111 100644
--- a/app/controllers/provider_interface/candidate_pool/candidates_controller.rb
+++ b/app/controllers/provider_interface/candidate_pool/candidates_controller.rb
@@ -33,7 +33,14 @@ def redirect_to_applications_unless_provider_opted_in
       end
 
       def filter_params
-        params.permit(:within, :original_location, visa_sponsorship: [])
+        params.permit(
+          :within,
+          :original_location,
+          subject: [],
+          study_mode: [],
+          course_type: [],
+          visa_sponsorship: [],
+        )
       end
     end
   end
diff --git a/app/frontend/packs/components/checkbox_search_filter.js b/app/frontend/packs/components/checkbox_search_filter.js
index 2dc3b6ec87c..0d9710b0d99 100644
--- a/app/frontend/packs/components/checkbox_search_filter.js
+++ b/app/frontend/packs/components/checkbox_search_filter.js
@@ -28,7 +28,7 @@ class CheckboxSearchFilter {
       if (labelValue.search(textValue) === -1) {
         allCheckboxes[i].style.display = 'none'
       } else {
-        allCheckboxes[i].style.display = 'block'
+        allCheckboxes[i].style.display = 'flex'
       }
     }
 
diff --git a/app/models/pool/candidates.rb b/app/models/pool/candidates.rb
index 1fbb8362630..54570805e18 100644
--- a/app/models/pool/candidates.rb
+++ b/app/models/pool/candidates.rb
@@ -23,6 +23,9 @@ def application_forms_for_provider
 
   def filtered_application_forms
     scope = curated_application_forms
+    scope = filter_by_subject(scope)
+    scope = filter_by_study_mode(scope)
+    scope = filter_by_course_type(scope)
     scope = filter_by_right_to_work_or_study(scope)
     filter_by_distance(scope)
   end
@@ -66,11 +69,42 @@ def filter_by_distance(scope)
       .select('application_forms.*', "#{calculate_distance_sql} AS site_distance")
   end
 
+  def filter_by_subject(scope)
+    return scope if filters[:subject].blank?
+
+    subjects = filters[:subject].flat_map { |value| value.split(',') }
+
+    scope.joins(application_choices: { course: :course_subjects })
+      .where(course_subjects: { subject_id: subjects })
+  end
+
+  def filter_by_study_mode(scope)
+    return scope if filters[:study_mode].blank?
+
+    scope.joins(application_choices: :course_option)
+      .where(course_option: { study_mode: filters[:study_mode] })
+  end
+
+  def filter_by_course_type(scope)
+    return scope if filters[:course_type].blank?
+
+    course_types = filters[:course_type].flat_map { |value| value.split(',') }
+
+    scope.joins(application_choices: :course)
+      .where(course: { program_type: course_types })
+  end
+
   def filter_by_right_to_work_or_study(scope)
     return scope if filters[:visa_sponsorship].blank?
 
     filter_values = filters[:visa_sponsorship].flat_map do |value|
-      value == 'required' ? 'no' : nil # required means no right_to_work_or_study, nil means yes
+      if value == 'required'
+        # required means no right_to_work_or_study
+        ApplicationForm.right_to_work_or_studies['no']
+      else
+        # else means all other enums + nil because we don't set this enum in most cases if candidate has right to work
+        ApplicationForm.right_to_work_or_studies.except('no').values << nil
+      end
     end
 
     scope.where(right_to_work_or_study: filter_values)
diff --git a/app/models/provider_interface/candidate_pool_filter.rb b/app/models/provider_interface/candidate_pool_filter.rb
index a968a328ed7..5ef45485f1c 100644
--- a/app/models/provider_interface/candidate_pool_filter.rb
+++ b/app/models/provider_interface/candidate_pool_filter.rb
@@ -21,6 +21,26 @@ def filters
           within: filter_params[:within],
           original_location: filter_params[:original_location],
         },
+        {
+          type: :checkbox_filter,
+          heading: 'Subject',
+          name: 'subject',
+          options: subject_options,
+          hide_tags: true,
+
+        },
+        {
+          type: :checkboxes,
+          heading: 'Study type',
+          name: 'study_mode',
+          options: study_mode_options,
+        },
+        {
+          type: :checkboxes,
+          heading: 'Course type',
+          name: 'course_type',
+          options: course_type_options,
+        },
         {
           type: :checkboxes,
           heading: 'Visa sponsorship',
@@ -64,5 +84,43 @@ def visa_sponsorship_options
         }
       end
     end
+
+    def subject_options
+      subjects = Subject.select("name, string_agg(id::text, ',') as ids").group(:name)
+
+      subjects.map do |subject|
+        {
+          value: subject.ids,
+          label: subject.name.capitalize,
+          checked: applied_filters[:subject]&.include?(subject.ids),
+        }
+      end
+    end
+
+    def study_mode_options
+      CourseOption.study_modes.map do |_, value|
+        {
+          value: value,
+          label: value.split('_').join(' ').capitalize,
+          checked: applied_filters[:study_mode]&.include?(value),
+        }
+      end
+    end
+
+    def course_type_options
+      %w[undergraduate postgraduate].map do |value|
+        filter_value = if value == 'postgraduate'
+                         Course.program_types.except('teacher_degree_apprenticeship').values.join(',')
+                       else
+                         Course.program_types['teacher_degree_apprenticeship']
+                       end
+
+        {
+          value: filter_value,
+          label: value.capitalize,
+          checked: applied_filters[:course_type]&.include?(filter_value),
+        }
+      end
+    end
   end
 end
diff --git a/spec/models/pool/candidates_spec.rb b/spec/models/pool/candidates_spec.rb
index d2866bd6c69..81d51b1f1b9 100644
--- a/spec/models/pool/candidates_spec.rb
+++ b/spec/models/pool/candidates_spec.rb
@@ -92,67 +92,175 @@
 
     context 'with filters' do
       it 'returns application_forms based on filters' do
-        providers = [create(:provider)]
-
-        manchester_candidate = create(:candidate, pool_status: 'opt_in')
-        manchester_candidate_form = create(:application_form, :completed, candidate: manchester_candidate)
+        provider = create(:provider)
         aa_teamworks = create(
           :site,
           latitude: 51.4524877,
           longitude: -0.1204749,
-          provider: providers.first,
+          provider:,
+        )
+        course = create(:course, provider:)
+        tda_course = create(
+          :course,
+          provider:,
+          program_type: :teacher_degree_apprenticeship,
         )
         course_option = create(
           :course_option,
           site: aa_teamworks,
-          course: create(:course, provider: providers.first),
+          course: course,
         )
-        create(:application_choice, :rejected, application_form: manchester_candidate_form, course_option:)
-
-        visa_sponsorship_candidate = create(:candidate, pool_status: 'opt_in')
-        visa_sponsorship_candidate_form = create(
-          :application_form,
-          :completed,
-          candidate: visa_sponsorship_candidate,
-          right_to_work_or_study: :no,
-        )
-        aa_teamworks = create(
-          :site,
-          latitude: 51.4524893,
-          longitude: -0.1204768,
-          provider: providers.first,
+        part_time_course_option = create(
+          :course_option,
+          site: aa_teamworks,
+          course: course,
+          study_mode: :part_time,
         )
-        course_option = create(
+        part_time_tda_course_option = create(
           :course_option,
           site: aa_teamworks,
-          course: create(:course, provider: providers.first),
+          course: tda_course,
+          study_mode: :part_time,
         )
-        create(:application_choice, :declined, application_form: visa_sponsorship_candidate_form, course_option:)
+
+        subject = create(:subject)
+        create(:course_subject, subject:, course:)
+        create(:course_subject, subject:, course: tda_course)
+
+        manchester_candidate_form = create_manchester_candidate_form(provider, aa_teamworks)
+        subject_candidate_form = create_subject_candidate_form(course_option)
+        part_time_candidate_form = create_part_time_candidate_form(part_time_course_option)
+        undergraduate_candidate_form = create_undergraduate_candidate_form(part_time_tda_course_option)
+        visa_sponsorship_candidate_form = create_visa_sponsorship_candidate_form(part_time_tda_course_option)
 
         withdrawn_candidate = create(:candidate, pool_status: 'opt_in')
         withdrawn_candidate_form = create(:application_form, :completed, candidate: withdrawn_candidate)
         create(:application_choice, :withdrawn, application_form: withdrawn_candidate_form)
 
         filters = { origin: [51.4524877, -0.1204749], within: 10 }
-        application_forms = described_class.application_forms_for_provider(providers:, filters:)
+        application_forms = described_class.application_forms_for_provider(providers: [provider], filters:)
 
-        expect(application_forms).to contain_exactly(
-          manchester_candidate_form,
-          visa_sponsorship_candidate_form,
+        expect(application_forms.map(&:id)).to contain_exactly(
+          manchester_candidate_form.id,
+          subject_candidate_form.id,
+          part_time_candidate_form.id,
+          undergraduate_candidate_form.id,
+          visa_sponsorship_candidate_form.id,
         )
 
         filters = {
           origin: [51.4524877, -0.1204749],
           within: 10,
+          subject: [subject.id.to_s],
+        }
+
+        application_forms = described_class.application_forms_for_provider(providers: [provider], filters:)
+
+        expect(application_forms.map(&:id)).to contain_exactly(
+          visa_sponsorship_candidate_form.id,
+          part_time_candidate_form.id,
+          subject_candidate_form.id,
+          undergraduate_candidate_form.id,
+        )
+
+        filters = {
+          origin: [51.4524877, -0.1204749],
+          within: 10,
+          subject: [subject.id.to_s],
+          study_mode: ['part_time'],
+        }
+
+        application_forms = described_class.application_forms_for_provider(providers: [provider], filters:)
+
+        expect(application_forms.map(&:id)).to contain_exactly(
+          visa_sponsorship_candidate_form.id,
+          part_time_candidate_form.id,
+          undergraduate_candidate_form.id,
+        )
+
+        filters = {
+          origin: [51.4524877, -0.1204749],
+          within: 10,
+          subject: [subject.id.to_s],
+          study_mode: ['part_time'],
+          course_type: ['TDA'],
+        }
+
+        application_forms = described_class.application_forms_for_provider(providers: [provider], filters:)
+
+        expect(application_forms.map(&:id)).to contain_exactly(
+          visa_sponsorship_candidate_form.id,
+          undergraduate_candidate_form.id,
+        )
+
+        filters = {
+          origin: [51.4524877, -0.1204749],
+          within: 10,
+          subject: [subject.id.to_s],
+          study_mode: ['part_time'],
+          course_type: ['TDA'],
           visa_sponsorship: ['required'],
         }
 
-        application_forms = described_class.application_forms_for_provider(providers:, filters:)
+        application_forms = described_class.application_forms_for_provider(providers: [provider], filters:)
 
-        expect(application_forms).to contain_exactly(
-          visa_sponsorship_candidate_form,
+        expect(application_forms.map(&:id)).to contain_exactly(
+          visa_sponsorship_candidate_form.id,
         )
       end
     end
+
+    def create_manchester_candidate_form(provider, aa_teamworks)
+      manchester_candidate = create(:candidate, pool_status: 'opt_in')
+      manchester_candidate_form = create(:application_form, :completed, candidate: manchester_candidate)
+      course = create(:course, provider:)
+      course_option = create(
+        :course_option,
+        site: aa_teamworks,
+        course: course,
+      )
+      create(:application_choice, :rejected, application_form: manchester_candidate_form, course_option:)
+
+      manchester_candidate_form
+    end
+
+    def create_subject_candidate_form(course_option)
+      subject_candidate = create(:candidate, pool_status: 'opt_in')
+      subject_candidate_form = create(:application_form, :completed, candidate: subject_candidate)
+      create(:application_choice, :rejected, application_form: subject_candidate_form, course_option:)
+
+      subject_candidate_form
+    end
+
+    def create_part_time_candidate_form(course_option)
+      part_time_candidate = create(:candidate, pool_status: 'opt_in')
+      part_time_candidate_form = create(:application_form, :completed, candidate: part_time_candidate)
+      create(:application_choice, :rejected, application_form: part_time_candidate_form, course_option:)
+      part_time_candidate_form
+    end
+
+    def create_undergraduate_candidate_form(course_option)
+      undergraduate_candidate = create(:candidate, pool_status: 'opt_in')
+      undergraduate_candidate_form = create(
+        :application_form,
+        :completed,
+        candidate: undergraduate_candidate,
+      )
+      create(:application_choice, :declined, application_form: undergraduate_candidate_form, course_option:)
+      undergraduate_candidate_form
+    end
+
+    def create_visa_sponsorship_candidate_form(course_option)
+      visa_sponsorship_candidate = create(:candidate, pool_status: 'opt_in')
+      visa_sponsorship_candidate_form = create(
+        :application_form,
+        :completed,
+        candidate: visa_sponsorship_candidate,
+        right_to_work_or_study: :no,
+      )
+      create(:application_choice, :declined, application_form: visa_sponsorship_candidate_form, course_option:)
+
+      visa_sponsorship_candidate_form
+    end
   end
 end
diff --git a/spec/models/provider_interface/candidate_pool_filter_spec.rb b/spec/models/provider_interface/candidate_pool_filter_spec.rb
index 25241aae3bb..cdeedd335b6 100644
--- a/spec/models/provider_interface/candidate_pool_filter_spec.rb
+++ b/spec/models/provider_interface/candidate_pool_filter_spec.rb
@@ -15,6 +15,47 @@
           within: nil,
           original_location: nil,
         },
+        {
+          type: :checkbox_filter,
+          heading: 'Subject',
+          name: 'subject',
+          options: [],
+          hide_tags: true,
+        },
+        {
+          type: :checkboxes,
+          heading: 'Study type',
+          name: 'study_mode',
+          options: [
+            {
+              value: 'full_time',
+              label: 'Full time',
+              checked: nil,
+            },
+            {
+              value: 'part_time',
+              label: 'Part time',
+              checked: nil,
+            },
+          ],
+        },
+        {
+          type: :checkboxes,
+          heading: 'Course type',
+          name: 'course_type',
+          options: [
+            {
+              value: 'TDA',
+              label: 'Undergraduate',
+              checked: nil,
+            },
+            {
+              value: 'HE,HES,SD,SS,SC,SSC,TA',
+              label: 'Postgraduate',
+              checked: nil,
+            },
+          ],
+        },
         {
           type: :checkboxes,
           heading: 'Visa sponsorship',


Reviews:
user: login: CatalinVoineag
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-06T10:09:52Z
user: login: CatalinVoineag
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-06T10:11:12Z
user: login: CatalinVoineag
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-06T10:13:48Z
user: login: elceebee
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-06T11:07:18Z
user: login: elceebee
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-06T11:16:39Z
user: login: CatalinVoineag
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-06T11:18:50Z
user: login: elceebee
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-06T11:20:38Z
user: login: CatalinVoineag
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-06T11:28:12Z
user: login: CatalinVoineag
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-06T11:29:55Z
user: login: CatalinVoineag
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-06T11:40:39Z
user: login: CatalinVoineag
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-06T11:43:07Z
user: login: elceebee
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-06T13:17:58Z
user: login: elceebee
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-06T13:18:21Z
user: login: elceebee
body: I've tested the filters and they all seem to work as expected now. There might be a few little design issues eg, I think these input boxes should probably line up. but I'm sure Pete or Rebecca will go through all that with a fine tooth comb. The functionality and code all looks good to me!
<img width="334" alt="image" src="https://github.com/user-attachments/assets/ae02e164-107a-4f5f-be55-5fcae6f4665f" />

state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-03-06T13:22:59Z

Review comments:
diff_hunk: @@ -66,11 +69,42 @@ def filter_by_distance(scope)
       .select('application_forms.*', "#{calculate_distance_sql} AS site_distance")
   end
 
+  def filter_by_subject(scope)
+    return scope if filters[:subject].blank?
+
+    subjects = filters[:subject].flat_map { |value| value.split(',') }
path: app/models/pool/candidates.rb
user: login: CatalinVoineag
body: We get the subject ids like ['1, 2, 3'] and we have to change it to ['1', '2', '3']

This is because checkbox values are not really made for passing multiple ids 🙃 
created_at: 2025-03-06T10:09:51Z
author_association: CONTRIBUTOR
diff_hunk: @@ -64,5 +84,43 @@ def visa_sponsorship_options
         }
       end
     end
+
+    def subject_options
+      subjects = Subject.select("name, string_agg(id::text, ',') as ids").group(:name)
path: app/models/provider_interface/candidate_pool_filter.rb
user: login: CatalinVoineag
body: Get all ids for the subjects with the same name
created_at: 2025-03-06T10:11:12Z
author_association: CONTRIBUTOR
diff_hunk: @@ -67,7 +67,7 @@
                     <% end %>
                      <% elsif filter[:type] == :checkbox_filter %>
                     <div id="<%= filter[:name] %>" class="app-checkbox-filter app-checkbox-filter--enhanced">
-                      <% if active_filters&.find { |f| f[:name] == filter[:name] } %>
+                      <% if active_filters&.find { |f| f[:name] == filter[:name] } && filter[:hide_tags].blank? %>
path: app/components/utility/filter_component.html.erb
user: login: CatalinVoineag
body: This is to remove the active filters being displayed over the subjects filter like
![Screenshot from 2025-03-06 10-13-02](https://github.com/user-attachments/assets/c3b9390d-ea9f-44e3-9249-c1fb1531b487)


We don't need this, we have the subjects at the top of the filters
created_at: 2025-03-06T10:13:48Z
author_association: CONTRIBUTOR
diff_hunk: @@ -33,7 +33,14 @@ def redirect_to_applications_unless_provider_opted_in
       end
 
       def filter_params
-        params.permit(:within, :original_location, visa_sponsorship: [])
+        params.permit(
path: app/controllers/provider_interface/candidate_pool/candidates_controller.rb
user: login: elceebee
body: should we be using `expect` here?
created_at: 2025-03-06T11:07:17Z
author_association: COLLABORATOR
diff_hunk: @@ -66,11 +69,42 @@ def filter_by_distance(scope)
       .select('application_forms.*', "#{calculate_distance_sql} AS site_distance")
   end
 
+  def filter_by_subject(scope)
+    return scope if filters[:subject].blank?
+
+    subjects = filters[:subject].flat_map { |value| value.split(',') }
+
+    scope.joins(application_choices: { course: :course_subjects })
+      .where(course_subjects: { subject_id: subjects })
+  end
+
+  def filter_by_study_mode(scope)
+    return scope if filters[:study_mode].blank?
+
+    scope.joins(application_choices: :course_option)
+      .where(course_option: { study_mode: filters[:study_mode] })
+  end
+
+  def filter_by_course_type(scope)
+    return scope if filters[:course_type].blank?
+
+    course_types = filters[:course_type].flat_map { |value| value.split(',') }
+
+    scope.joins(application_choices: :course)
+      .where(course: { program_type: course_types })
+  end
+
   def filter_by_right_to_work_or_study(scope)
     return scope if filters[:visa_sponsorship].blank?
 
     filter_values = filters[:visa_sponsorship].flat_map do |value|
-      value == 'required' ? 'no' : nil # required means no right_to_work_or_study, nil means yes
+      if value == 'required'
+        # required means no right_to_work_or_study
+        ApplicationForm.right_to_work_or_studies['no']
+      else
+        # else means all other enums + nil because we don't set this enum in most cases if candidate has right to work
+        ApplicationForm.right_to_work_or_studies.except('no').values << nil
+      end
path: app/models/pool/candidates.rb
user: login: elceebee
body: I feel like this isn't quite working as expected. See video. I would expect the first person on the list to be there when I filter by 'required'.


https://github.com/user-attachments/assets/df62165a-e38b-4705-a185-69d4037bbf49




created_at: 2025-03-06T11:16:39Z
author_association: COLLABORATOR
diff_hunk: @@ -66,11 +69,42 @@ def filter_by_distance(scope)
       .select('application_forms.*', "#{calculate_distance_sql} AS site_distance")
   end
 
+  def filter_by_subject(scope)
+    return scope if filters[:subject].blank?
+
+    subjects = filters[:subject].flat_map { |value| value.split(',') }
+
+    scope.joins(application_choices: { course: :course_subjects })
+      .where(course_subjects: { subject_id: subjects })
+  end
+
+  def filter_by_study_mode(scope)
+    return scope if filters[:study_mode].blank?
+
+    scope.joins(application_choices: :course_option)
+      .where(course_option: { study_mode: filters[:study_mode] })
+  end
+
+  def filter_by_course_type(scope)
+    return scope if filters[:course_type].blank?
+
+    course_types = filters[:course_type].flat_map { |value| value.split(',') }
+
+    scope.joins(application_choices: :course)
+      .where(course: { program_type: course_types })
+  end
+
   def filter_by_right_to_work_or_study(scope)
     return scope if filters[:visa_sponsorship].blank?
 
     filter_values = filters[:visa_sponsorship].flat_map do |value|
-      value == 'required' ? 'no' : nil # required means no right_to_work_or_study, nil means yes
+      if value == 'required'
+        # required means no right_to_work_or_study
+        ApplicationForm.right_to_work_or_studies['no']
+      else
+        # else means all other enums + nil because we don't set this enum in most cases if candidate has right to work
+        ApplicationForm.right_to_work_or_studies.except('no').values << nil
+      end
path: app/models/pool/candidates.rb
user: login: CatalinVoineag
body: You posted your comment too soon, the video has not uploaded 😄 
created_at: 2025-03-06T11:18:50Z
author_association: CONTRIBUTOR
diff_hunk: @@ -64,5 +84,43 @@ def visa_sponsorship_options
         }
       end
     end
+
+    def subject_options
+      subjects = Subject.select("name, string_agg(id::text, ',') as ids").group(:name)
+
+      subjects.map do |subject|
+        {
+          value: subject.ids,
+          label: subject.name.capitalize,
+          checked: applied_filters[:subject]&.include?(subject.ids),
+        }
+      end
+    end
+
+    def study_mode_options
+      CourseOption.study_modes.map do |_, value|
+        {
+          value: value,
+          label: value.split('_').join(' ').capitalize,
+          checked: applied_filters[:study_mode]&.include?(value),
+        }
+      end
+    end
+
+    def course_type_options
+      %w[undergraduate postgraduate].map do |value|
path: app/models/provider_interface/candidate_pool_filter.rb
user: login: elceebee
body: I don't think I'm understanding how this one should work either. See video, I want people who want to do undergraduate courses, but the results are people with degrees already who have applied to post graduate courses.


https://github.com/user-attachments/assets/138774db-8cdd-4a69-8034-b476996a9f4a


created_at: 2025-03-06T11:20:38Z
author_association: COLLABORATOR
diff_hunk: @@ -66,11 +69,42 @@ def filter_by_distance(scope)
       .select('application_forms.*', "#{calculate_distance_sql} AS site_distance")
   end
 
+  def filter_by_subject(scope)
+    return scope if filters[:subject].blank?
+
+    subjects = filters[:subject].flat_map { |value| value.split(',') }
+
+    scope.joins(application_choices: { course: :course_subjects })
+      .where(course_subjects: { subject_id: subjects })
+  end
+
+  def filter_by_study_mode(scope)
+    return scope if filters[:study_mode].blank?
+
+    scope.joins(application_choices: :course_option)
+      .where(course_option: { study_mode: filters[:study_mode] })
+  end
+
+  def filter_by_course_type(scope)
+    return scope if filters[:course_type].blank?
+
+    course_types = filters[:course_type].flat_map { |value| value.split(',') }
+
+    scope.joins(application_choices: :course)
+      .where(course: { program_type: course_types })
+  end
+
   def filter_by_right_to_work_or_study(scope)
     return scope if filters[:visa_sponsorship].blank?
 
     filter_values = filters[:visa_sponsorship].flat_map do |value|
-      value == 'required' ? 'no' : nil # required means no right_to_work_or_study, nil means yes
+      if value == 'required'
+        # required means no right_to_work_or_study
+        ApplicationForm.right_to_work_or_studies['no']
+      else
+        # else means all other enums + nil because we don't set this enum in most cases if candidate has right to work
+        ApplicationForm.right_to_work_or_studies.except('no').values << nil
+      end
path: app/models/pool/candidates.rb
user: login: CatalinVoineag
body: The show page visa sponsorship component is broken because of the same issue with nil values in DB. I have a note for myself to come back and fix it. The candidate 41, which is the one you inspected has right_to_work_or_study nil. Which means they don't need visa sponsorship 🙃 

The show page always says required until I fix it

![Screenshot from 2025-03-06 11-27-29](https://github.com/user-attachments/assets/0b6a536c-c14b-4980-8030-5e3ebd787142)

created_at: 2025-03-06T11:28:12Z
author_association: CONTRIBUTOR
diff_hunk: @@ -66,11 +69,42 @@ def filter_by_distance(scope)
       .select('application_forms.*', "#{calculate_distance_sql} AS site_distance")
   end
 
+  def filter_by_subject(scope)
+    return scope if filters[:subject].blank?
+
+    subjects = filters[:subject].flat_map { |value| value.split(',') }
+
+    scope.joins(application_choices: { course: :course_subjects })
+      .where(course_subjects: { subject_id: subjects })
+  end
+
+  def filter_by_study_mode(scope)
+    return scope if filters[:study_mode].blank?
+
+    scope.joins(application_choices: :course_option)
+      .where(course_option: { study_mode: filters[:study_mode] })
+  end
+
+  def filter_by_course_type(scope)
+    return scope if filters[:course_type].blank?
+
+    course_types = filters[:course_type].flat_map { |value| value.split(',') }
+
+    scope.joins(application_choices: :course)
+      .where(course: { program_type: course_types })
+  end
+
   def filter_by_right_to_work_or_study(scope)
     return scope if filters[:visa_sponsorship].blank?
 
     filter_values = filters[:visa_sponsorship].flat_map do |value|
-      value == 'required' ? 'no' : nil # required means no right_to_work_or_study, nil means yes
+      if value == 'required'
+        # required means no right_to_work_or_study
+        ApplicationForm.right_to_work_or_studies['no']
+      else
+        # else means all other enums + nil because we don't set this enum in most cases if candidate has right to work
+        ApplicationForm.right_to_work_or_studies.except('no').values << nil
+      end
path: app/models/pool/candidates.rb
user: login: CatalinVoineag
body: I have changed the data on the review app and now you will see that candidate if you use the required visa sponsorship filter
created_at: 2025-03-06T11:29:55Z
author_association: CONTRIBUTOR
diff_hunk: @@ -64,5 +84,43 @@ def visa_sponsorship_options
         }
       end
     end
+
+    def subject_options
+      subjects = Subject.select("name, string_agg(id::text, ',') as ids").group(:name)
+
+      subjects.map do |subject|
+        {
+          value: subject.ids,
+          label: subject.name.capitalize,
+          checked: applied_filters[:subject]&.include?(subject.ids),
+        }
+      end
+    end
+
+    def study_mode_options
+      CourseOption.study_modes.map do |_, value|
+        {
+          value: value,
+          label: value.split('_').join(' ').capitalize,
+          checked: applied_filters[:study_mode]&.include?(value),
+        }
+      end
+    end
+
+    def course_type_options
+      %w[undergraduate postgraduate].map do |value|
path: app/models/provider_interface/candidate_pool_filter.rb
user: login: CatalinVoineag
body: You're right, I got my logic wrong, nice catch 💯 
created_at: 2025-03-06T11:40:39Z
author_association: CONTRIBUTOR
diff_hunk: @@ -33,7 +33,14 @@ def redirect_to_applications_unless_provider_opted_in
       end
 
       def filter_params
-        params.permit(:within, :original_location, visa_sponsorship: [])
+        params.permit(
path: app/controllers/provider_interface/candidate_pool/candidates_controller.rb
user: login: CatalinVoineag
body: Don't think so, `expect` requires params to be present but some params don't need to be present. I think `expect` can replace `.require.permit`  

![Screenshot from 2025-03-06 11-41-40](https://github.com/user-attachments/assets/959958a9-7012-474f-bd20-68846187fc73)

created_at: 2025-03-06T11:43:07Z
author_association: CONTRIBUTOR
diff_hunk: @@ -66,11 +69,42 @@ def filter_by_distance(scope)
       .select('application_forms.*', "#{calculate_distance_sql} AS site_distance")
   end
 
+  def filter_by_subject(scope)
+    return scope if filters[:subject].blank?
+
+    subjects = filters[:subject].flat_map { |value| value.split(',') }
+
+    scope.joins(application_choices: { course: :course_subjects })
+      .where(course_subjects: { subject_id: subjects })
+  end
+
+  def filter_by_study_mode(scope)
+    return scope if filters[:study_mode].blank?
+
+    scope.joins(application_choices: :course_option)
+      .where(course_option: { study_mode: filters[:study_mode] })
+  end
+
+  def filter_by_course_type(scope)
+    return scope if filters[:course_type].blank?
+
+    course_types = filters[:course_type].flat_map { |value| value.split(',') }
+
+    scope.joins(application_choices: :course)
+      .where(course: { program_type: course_types })
+  end
+
   def filter_by_right_to_work_or_study(scope)
     return scope if filters[:visa_sponsorship].blank?
 
     filter_values = filters[:visa_sponsorship].flat_map do |value|
-      value == 'required' ? 'no' : nil # required means no right_to_work_or_study, nil means yes
+      if value == 'required'
+        # required means no right_to_work_or_study
+        ApplicationForm.right_to_work_or_studies['no']
+      else
+        # else means all other enums + nil because we don't set this enum in most cases if candidate has right to work
+        ApplicationForm.right_to_work_or_studies.except('no').values << nil
+      end
path: app/models/pool/candidates.rb
user: login: elceebee
body: Cool, thanks for explaining it! 
created_at: 2025-03-06T13:17:58Z
author_association: COLLABORATOR
diff_hunk: @@ -64,5 +84,43 @@ def visa_sponsorship_options
         }
       end
     end
+
+    def subject_options
+      subjects = Subject.select("name, string_agg(id::text, ',') as ids").group(:name)
+
+      subjects.map do |subject|
+        {
+          value: subject.ids,
+          label: subject.name.capitalize,
+          checked: applied_filters[:subject]&.include?(subject.ids),
+        }
+      end
+    end
+
+    def study_mode_options
+      CourseOption.study_modes.map do |_, value|
+        {
+          value: value,
+          label: value.split('_').join(' ').capitalize,
+          checked: applied_filters[:study_mode]&.include?(value),
+        }
+      end
+    end
+
+    def course_type_options
+      %w[undergraduate postgraduate].map do |value|
path: app/models/provider_interface/candidate_pool_filter.rb
user: login: elceebee
body: Easily done!

created_at: 2025-03-06T13:18:21Z
author_association: COLLABORATOR

PR comments:
user: login: github-actions[bot]
created_at: 2025-03-05T15:16:45Z
body: <h2>You have one or more flakey tests on this branch! :snowflake: :snowflake: :snowflake:</h2>Failed 1 out of 3 times at <a href="/DFE-Digital/apply-for-teacher-training/blob/other-pool-filters/./spec/system/provider_interface/candidate_pool/provider_views_candidate_pool_list_spec.rb#L15">./spec/system/provider_interface/candidate_pool/provider_views_candidate_pool_list_spec.rb:15</a>: :warning: Expected [33m["L***** Z*****", "A***** T*****"][0m to eq [35m["A***** T*****", "L***** Z*****"][0m.

[37mDiff:[0m

[34m┌ (Key) ──────────────────────────┐[0m
[34m│ [0m[35m‹-› in expected, not in actual[0m[34m  │[0m
[34m│ [0m[33m‹+› in actual, not in expected[0m[34m  │[0m
[34m│ [0m‹ › in both expected and actual[34m │[0m
[34m└─────────────────────────────────┘[0m

  [
[35m-   "A***** T*****",[0m
    "L***** Z*****",
[33m+   "A***** T*****"[0m
  ]<br>
author_association: CONTRIBUTOR
user: login: CatalinVoineag
created_at: 2025-03-06T10:05:28Z
body: ![](https://github.trello.services/images/mini-trello-icon.png) [Find a candidate - Build our proposed MVP ready for testing with Providers](https://trello.com/c/PfQsLFsf/651-find-a-candidate-build-our-proposed-mvp-ready-for-testing-with-providers)
author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-03-06T11:01:13Z
body: <h2>You have one or more flakey tests on this branch! :snowflake: :snowflake: :snowflake:</h2>Failed 1 out of 3 times at <a href="/DFE-Digital/apply-for-teacher-training/blob/other-pool-filters/./spec/system/provider_interface/candidate_pool/provider_views_candidate_pool_list_spec.rb#L15">./spec/system/provider_interface/candidate_pool/provider_views_candidate_pool_list_spec.rb:15</a>: :warning: Expected [33m["M***** D*****", "E***** B*****"][0m to eq [35m["E***** B*****", "M***** D*****"][0m.

[37mDiff:[0m

[34m┌ (Key) ──────────────────────────┐[0m
[34m│ [0m[35m‹-› in expected, not in actual[0m[34m  │[0m
[34m│ [0m[33m‹+› in actual, not in expected[0m[34m  │[0m
[34m│ [0m‹ › in both expected and actual[34m │[0m
[34m└─────────────────────────────────┘[0m

  [
[35m-   "E***** B*****",[0m
    "M***** D*****",
[33m+   "E***** B*****"[0m
  ]<br>
author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-03-06T11:03:50Z
body: <h2>You have one or more flakey tests on this branch! :snowflake: :snowflake: :snowflake:</h2>Failed 1 out of 3 times at <a href="/DFE-Digital/apply-for-teacher-training/blob/other-pool-filters/./spec/system/provider_interface/candidate_pool/provider_views_candidate_pool_list_spec.rb#L15">./spec/system/provider_interface/candidate_pool/provider_views_candidate_pool_list_spec.rb:15</a>: :warning: Expected [33m["M***** D*****", "E***** B*****"][0m to eq [35m["E***** B*****", "M***** D*****"][0m.

[37mDiff:[0m

[34m┌ (Key) ──────────────────────────┐[0m
[34m│ [0m[35m‹-› in expected, not in actual[0m[34m  │[0m
[34m│ [0m[33m‹+› in actual, not in expected[0m[34m  │[0m
[34m│ [0m‹ › in both expected and actual[34m │[0m
[34m└─────────────────────────────────┘[0m

  [
[35m-   "E***** B*****",[0m
    "M***** D*****",
[33m+   "E***** B*****"[0m
  ]<br>
author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-03-06T12:56:29Z
body: <h2>You have one or more flakey tests on this branch! :snowflake: :snowflake: :snowflake:</h2>Failed 1 out of 3 times at <a href="/DFE-Digital/apply-for-teacher-training/blob/other-pool-filters/./spec/system/provider_interface/candidate_pool/provider_views_candidate_pool_list_spec.rb#L15">./spec/system/provider_interface/candidate_pool/provider_views_candidate_pool_list_spec.rb:15</a>: :warning: Expected [33m["G***** H*****", "A***** H*****"][0m to eq [35m["A***** H*****", "G***** H*****"][0m.

[37mDiff:[0m

[34m┌ (Key) ──────────────────────────┐[0m
[34m│ [0m[35m‹-› in expected, not in actual[0m[34m  │[0m
[34m│ [0m[33m‹+› in actual, not in expected[0m[34m  │[0m
[34m│ [0m‹ › in both expected and actual[34m │[0m
[34m└─────────────────────────────────┘[0m

  [
[35m-   "A***** H*****",[0m
    "G***** H*****",
[33m+   "A***** H*****"[0m
  ]<br>
author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-03-06T13:00:53Z
body: <h2>You have one or more flakey tests on this branch! :snowflake: :snowflake: :snowflake:</h2>Failed 1 out of 3 times at <a href="/DFE-Digital/apply-for-teacher-training/blob/other-pool-filters/./spec/system/provider_interface/candidate_pool/provider_views_candidate_pool_list_spec.rb#L15">./spec/system/provider_interface/candidate_pool/provider_views_candidate_pool_list_spec.rb:15</a>: :warning: Expected [33m["G***** H*****", "A***** H*****"][0m to eq [35m["A***** H*****", "G***** H*****"][0m.

[37mDiff:[0m

[34m┌ (Key) ──────────────────────────┐[0m
[34m│ [0m[35m‹-› in expected, not in actual[0m[34m  │[0m
[34m│ [0m[33m‹+› in actual, not in expected[0m[34m  │[0m
[34m│ [0m‹ › in both expected and actual[34m │[0m
[34m└─────────────────────────────────┘[0m

  [
[35m-   "A***** H*****",[0m
    "G***** H*****",
[33m+   "A***** H*****"[0m
  ]<br>
author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-03-06T13:03:54Z
body: <h2>You have one or more flakey tests on this branch! :snowflake: :snowflake: :snowflake:</h2>Failed 1 out of 3 times at <a href="/DFE-Digital/apply-for-teacher-training/blob/other-pool-filters/./spec/system/provider_interface/candidate_pool/provider_views_candidate_pool_list_spec.rb#L15">./spec/system/provider_interface/candidate_pool/provider_views_candidate_pool_list_spec.rb:15</a>: :warning: Expected [33m["G***** H*****", "A***** H*****"][0m to eq [35m["A***** H*****", "G***** H*****"][0m.

[37mDiff:[0m

[34m┌ (Key) ──────────────────────────┐[0m
[34m│ [0m[35m‹-› in expected, not in actual[0m[34m  │[0m
[34m│ [0m[33m‹+› in actual, not in expected[0m[34m  │[0m
[34m│ [0m‹ › in both expected and actual[34m │[0m
[34m└─────────────────────────────────┘[0m

  [
[35m-   "A***** H*****",[0m
    "G***** H*****",
[33m+   "A***** H*****"[0m
  ]<br>
author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-03-06T13:17:38Z
body: <h2>You have one or more flakey tests on this branch! :snowflake: :snowflake: :snowflake:</h2>Failed 1 out of 3 times at <a href="/DFE-Digital/apply-for-teacher-training/blob/other-pool-filters/./spec/system/provider_interface/candidate_pool/provider_views_candidate_pool_list_spec.rb#L15">./spec/system/provider_interface/candidate_pool/provider_views_candidate_pool_list_spec.rb:15</a>: :warning: Expected [33m["G***** H*****", "A***** H*****"][0m to eq [35m["A***** H*****", "G***** H*****"][0m.

[37mDiff:[0m

[34m┌ (Key) ──────────────────────────┐[0m
[34m│ [0m[35m‹-› in expected, not in actual[0m[34m  │[0m
[34m│ [0m[33m‹+› in actual, not in expected[0m[34m  │[0m
[34m│ [0m‹ › in both expected and actual[34m │[0m
[34m└─────────────────────────────────┘[0m

  [
[35m-   "A***** H*****",[0m
    "G***** H*****",
[33m+   "A***** H*****"[0m
  ]<br>
author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-03-06T13:19:23Z
body: <h2>You have one or more flakey tests on this branch! :snowflake: :snowflake: :snowflake:</h2>Failed 1 out of 3 times at <a href="/DFE-Digital/apply-for-teacher-training/blob/other-pool-filters/./spec/system/provider_interface/candidate_pool/provider_views_candidate_pool_list_spec.rb#L15">./spec/system/provider_interface/candidate_pool/provider_views_candidate_pool_list_spec.rb:15</a>: :warning: Expected [33m["G***** H*****", "A***** H*****"][0m to eq [35m["A***** H*****", "G***** H*****"][0m.

[37mDiff:[0m

[34m┌ (Key) ──────────────────────────┐[0m
[34m│ [0m[35m‹-› in expected, not in actual[0m[34m  │[0m
[34m│ [0m[33m‹+› in actual, not in expected[0m[34m  │[0m
[34m│ [0m‹ › in both expected and actual[34m │[0m
[34m└─────────────────────────────────┘[0m

  [
[35m-   "A***** H*****",[0m
    "G***** H*****",
[33m+   "A***** H*****"[0m
  ]<br>
author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-03-06T14:21:05Z
body: <h2>You have one or more flakey tests on this branch! :snowflake: :snowflake: :snowflake:</h2>Failed 1 out of 3 times at <a href="/DFE-Digital/apply-for-teacher-training/blob/other-pool-filters/./spec/system/provider_interface/candidate_pool/provider_views_candidate_pool_list_spec.rb#L15">./spec/system/provider_interface/candidate_pool/provider_views_candidate_pool_list_spec.rb:15</a>: :warning: Expected [33m["G***** H*****", "A***** H*****"][0m to eq [35m["A***** H*****", "G***** H*****"][0m.

[37mDiff:[0m

[34m┌ (Key) ──────────────────────────┐[0m
[34m│ [0m[35m‹-› in expected, not in actual[0m[34m  │[0m
[34m│ [0m[33m‹+› in actual, not in expected[0m[34m  │[0m
[34m│ [0m‹ › in both expected and actual[34m │[0m
[34m└─────────────────────────────────┘[0m

  [
[35m-   "A***** H*****",[0m
    "G***** H*****",
[33m+   "A***** H*****"[0m
  ]<br>
author_association: CONTRIBUTOR