Skip to content

Commit 3788dff

Browse files
Hiring staff jobs filter (#8505)
## Trello card URL https://trello.com/c/F6AbcA2w/2567-update-filter-bar-for-la-mat-users-for-job-listing-management ## Changes in this PR: This PR adds: - ability for hiring staff to filter their jobs by job role - changes link to publisher preferences page to a green button - adds hint text to schools filter - adds search functionality to filter options - fixes potential security issue which could allow hiring staff at one organisations to see another organisations jobs ## Screenshots of UI changes: ### Before ### After ## Checklists: ### Data & Schema Changes If this PR modifies data structures or validations, check the following: - [ ] Adds/removes model validations - [ ] Adds/removes database fields - [ ] Modifies Vacancy enumerables (phases, working patterns, job roles, key stages, etc.) <details> <summary>If any of the above options has changed then the author must check/resolve all of the following...</summary> ### Integration Impact Does this change affect any of these integrations? - [ ] DfE Analytics platform - [ ] Legacy imports mappings - [ ] DWP Find a Job export mappings - [ ] Publisher ATS API (may require mapping updates or API versioning) ### User Experience & Data Integrity Could this change impact: - [ ] Existing subscription alerts (will legacy subscription search filters break?) - [ ] Legacy vacancy copying (will copied vacancies fail new validations?) - [ ] In-progress drafts for Vacancies or Job Applications </details>
1 parent 8b9c746 commit 3788dff

File tree

11 files changed

+138
-40
lines changed

11 files changed

+138
-40
lines changed

app/components/dashboard_component.rb

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ class DashboardComponent < ApplicationComponent
33
include VacanciesHelper
44

55
# rubocop:disable Metrics/ParameterLists
6-
def initialize(organisation:, sort:, selected_type:, publisher_preference:, vacancies:, count:, vacancy_types:, filter_form:, selected_organisation_ids: [])
6+
def initialize(organisation:, sort:, selected_type:, publisher_preference:, vacancies:, count:, vacancy_types:, filter_form:, selected_organisation_ids: [], selected_job_roles: [])
77
# rubocop:enable Metrics/ParameterLists
88
super(classes: [], html_attributes: {})
99
@organisation = organisation
@@ -13,10 +13,12 @@ def initialize(organisation:, sort:, selected_type:, publisher_preference:, vaca
1313
@vacancy_types = vacancy_types
1414
@selected_type = selected_type
1515
@selected_organisation_ids = selected_organisation_ids
16+
@selected_job_roles = selected_job_roles
1617
@filter_form = filter_form
1718

1819
@vacancies = vacancies
1920
set_organisation_options if @organisation.school_group?
21+
set_job_role_options
2022
@count = count
2123
end
2224

@@ -25,7 +27,8 @@ def grid_column_class
2527
end
2628

2729
def no_jobs_text
28-
t("jobs.manage.#{selected_type}.no_jobs.#{@selected_organisation_ids.any? ? 'with' : 'no'}_filters")
30+
has_filters = @selected_organisation_ids.any? || @selected_job_roles.any?
31+
t("jobs.manage.#{selected_type}.no_jobs.#{has_filters ? 'with' : 'no'}_filters")
2932
end
3033

3134
def view_applicants(vacancy, job_applications_count)
@@ -37,7 +40,7 @@ def view_applicants(vacancy, job_applications_count)
3740

3841
private
3942

40-
attr_reader :publisher_preference, :organisation, :selected_type, :sort, :vacancies, :selected_organisation_ids
43+
attr_reader :publisher_preference, :organisation, :selected_type, :sort, :vacancies, :selected_organisation_ids, :selected_job_roles
4144

4245
def set_organisation_options
4346
schools = organisation.local_authority? ? publisher_preference.schools : organisation.schools
@@ -54,6 +57,18 @@ def set_organisation_options
5457
)
5558
end
5659

60+
def set_job_role_options
61+
teaching_roles = Vacancy::TEACHING_JOB_ROLES.map do |role|
62+
Option.new(id: role, name: role, label: I18n.t("helpers.label.publishers_job_listing_job_role_form.teaching_job_role_options.#{role}"))
63+
end
64+
65+
support_roles = Vacancy::SUPPORT_JOB_ROLES.map do |role|
66+
Option.new(id: role, name: role, label: I18n.t("helpers.label.publishers_job_listing_job_role_form.support_job_role_options.#{role}"))
67+
end
68+
69+
@job_role_options = teaching_roles + support_roles
70+
end
71+
5772
def default_classes
5873
%w[dashboard-component]
5974
end

app/components/dashboard_component/dashboard_component.html.slim

Lines changed: 27 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -30,26 +30,43 @@
3030
- if @organisation.school_group?
3131
.govuk-grid-column-one-third-at-desktop class="govuk-!-margin-bottom-4"
3232
- if @organisation.local_authority?
33-
= govuk_link_to t("jobs.dashboard.add_or_remove_schools"), edit_publishers_publisher_preference_path(@publisher_preference), class: "govuk-link--no-visited-state"
33+
= govuk_button_link_to t("jobs.dashboard.add_or_remove_schools"), edit_publishers_publisher_preference_path(@publisher_preference)
3434

3535
div class="govuk-!-margin-top-2"
3636
= form_for @filter_form, as: "", url: organisation_jobs_with_type_path(type: @selected_type), method: :get, html: { data: { controller: "form", "hide-submit": true } } do |f|
3737
= filters(submit_button: f.govuk_submit(t("buttons.apply_filters")),
38-
filters: { total_count: @selected_organisation_ids.size },
38+
filters: { total_count: @selected_organisation_ids.size + @selected_job_roles.size },
3939
clear_filters_link: { text: t("shared.filter_group.clear_all_filters"), url: organisation_jobs_with_type_path(type: @selected_type) },
4040
options: { remove_filter_links: true },
4141
html_attributes: { tabindex: "-1" }) do |filters_component|
42-
- if @selected_organisation_ids.any?
42+
- if @selected_organisation_ids.any? || @selected_job_roles.any?
4343
- filters_component.with_remove_filter_links do |rb|
44-
- rb.with_group(key: "organisation_ids",
45-
selected: @selected_organisation_ids,
46-
options: @organisation_options,
47-
value_method: :id,
48-
selected_method: :name,
49-
remove_filter_link: { url_helper: :organisation_jobs_with_type_path, params: { "type" => @selected_type, "organisation_ids" => @selected_organisation_ids } })
44+
- if @selected_organisation_ids.any?
45+
- rb.with_group(key: "organisation_ids",
46+
selected: @selected_organisation_ids,
47+
options: @organisation_options,
48+
value_method: :id,
49+
selected_method: :name,
50+
remove_filter_link: { url_helper: :organisation_jobs_with_type_path, params: { "type" => @selected_type, "organisation_ids" => @selected_organisation_ids, "job_roles" => @selected_job_roles } })
51+
- if @selected_job_roles.any?
52+
- rb.with_group(key: "job_roles",
53+
selected: @selected_job_roles,
54+
options: @job_role_options,
55+
value_method: :id,
56+
selected_method: :label,
57+
remove_filter_link: { url_helper: :organisation_jobs_with_type_path, params: { "type" => @selected_type, "organisation_ids" => @selected_organisation_ids, "job_roles" => @selected_job_roles } })
5058

5159
- filters_component.with_group key: "locations",
52-
component: f.govuk_collection_check_boxes(:organisation_ids, @organisation_options, :id, :label, small: true, legend: { text: "Locations", tag: "h2" }, hint: nil, form_group: { data: { action: "change->form#submitListener" } })
60+
component: searchable_collection(collection: f.govuk_collection_check_boxes(:organisation_ids, @organisation_options, :id, :label, small: true, legend: { text: "Locations", tag: "h2" }, hint: (@organisation.local_authority? ? { text: t("jobs.dashboard.location_filter_hint") } : nil), form_group: { data: { action: "change->form#submitListener" } }),
61+
collection_count: @organisation_options.count,
62+
options: { scrollable: true },
63+
text: { aria_label: "Locations", placeholder: t("helpers.hint.publishers_vacancy_filter_form.organisation_ids_placeholder") })
64+
65+
- filters_component.with_group key: "job_roles",
66+
component: searchable_collection(collection: f.govuk_collection_check_boxes(:job_roles, @job_role_options, :id, :label, small: true, legend: { text: "Job roles", tag: "h2" }, hint: nil, form_group: { data: { action: "change->form#submitListener" } }),
67+
collection_count: @job_role_options.count,
68+
options: { scrollable: true },
69+
text: { aria_label: "Job roles", placeholder: t("helpers.hint.publishers_vacancy_filter_form.job_roles_placeholder") })
5370

5471
.help-guide--desktop class="govuk-!-margin-top-4"
5572
h2.govuk-heading-m = t("jobs.dashboard.how_to_accept_job_applications_guide.title")

app/controllers/publishers/vacancies_controller.rb

Lines changed: 30 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -27,33 +27,20 @@ def show
2727
awaiting_feedback: :awaiting_feedback_recently_expired,
2828
}.freeze
2929

30-
# rubocop:disable Metrics/AbcSize
3130
def index
3231
@selected_type = (params[:type] || :live).to_sym
3332
@sort = Publishers::VacancySort.new(current_organisation, @selected_type).update(sort_by: params[:sort_by])
34-
scope = if @selected_type == :draft
35-
DraftVacancy.kept.where.not(job_title: nil)
36-
else
37-
PublishedVacancy.kept.public_send(VACANCY_TYPES.fetch(@selected_type))
38-
end
39-
40-
accessible_org_ids = current_publisher.accessible_organisations(current_organisation).map(&:id)
4133

42-
# Apply organisation filter from URL params if present, otherwise show all accessible
43-
@selected_organisation_ids = params[:organisation_ids]&.reject(&:blank?) || []
44-
org_ids_to_filter = @selected_organisation_ids.any? ? @selected_organisation_ids : accessible_org_ids
45-
46-
vacancies = scope
47-
.in_organisation_ids(org_ids_to_filter)
48-
.order(@sort.by => @sort.order)
34+
vacancies = apply_vacancy_filters
4935

5036
@pagy, @vacancies = pagy(vacancies)
5137
@count = vacancies.count
52-
5338
@vacancy_types = VACANCY_TYPES.keys
54-
@filter_form = Publishers::VacancyFilterForm.new(organisation_ids: @selected_organisation_ids)
39+
@filter_form = Publishers::VacancyFilterForm.new(
40+
organisation_ids: @selected_organisation_ids,
41+
job_roles: @selected_job_roles,
42+
)
5543
end
56-
# rubocop:enable Metrics/AbcSize
5744

5845
# We don't save anything here - just redirect to the show page
5946
def save_and_finish_later
@@ -119,6 +106,30 @@ def show_application_feature_reminder_page?
119106
).none?
120107
end
121108

109+
def apply_vacancy_filters
110+
scope = if @selected_type == :draft
111+
DraftVacancy.kept.where.not(job_title: nil)
112+
else
113+
PublishedVacancy.kept.public_send(VACANCY_TYPES.fetch(@selected_type))
114+
end
115+
116+
accessible_org_ids = current_publisher.accessible_organisations(current_organisation).map(&:id)
117+
118+
# Only allow filtering by organisations the user has access to
119+
requested_org_ids = params[:organisation_ids]&.reject(&:blank?) || []
120+
@selected_organisation_ids = requested_org_ids & accessible_org_ids.map(&:to_s)
121+
org_ids_to_filter = @selected_organisation_ids.any? ? @selected_organisation_ids : accessible_org_ids
122+
123+
vacancies = scope
124+
.in_organisation_ids(org_ids_to_filter)
125+
.order(@sort.by => @sort.order)
126+
127+
@selected_job_roles = params[:job_roles]&.reject(&:blank?) || []
128+
vacancies = vacancies.with_any_of_job_roles(@selected_job_roles) if @selected_job_roles.any?
129+
130+
vacancies
131+
end
132+
122133
def set_publisher_preference
123134
if current_organisation.local_authority? # Local Authorities publisher need to set their preference for the local authority
124135
@publisher_preference = PublisherPreference.find_by(publisher: current_publisher, organisation: current_organisation)
@@ -151,5 +162,6 @@ def redirect_to_show_publisher_profile_incomplete
151162

152163
def strip_empty_checkbox_params
153164
params[:organisation_ids]&.reject!(&:blank?)
165+
params[:job_roles]&.reject!(&:blank?)
154166
end
155167
end
Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,17 @@
11
class Publishers::VacancyFilterForm
22
include ActiveModel::Model
33

4-
attr_accessor :organisation_ids
4+
attr_accessor :organisation_ids, :job_roles
55

6-
def initialize(params = {})
7-
@organisation_ids = params[:organisation_ids] || []
6+
def initialize(organisation_ids: [], job_roles: [])
7+
@organisation_ids = organisation_ids
8+
@job_roles = job_roles
89
end
910

1011
def to_hash
1112
{
1213
organisation_ids: @organisation_ids,
14+
job_roles: @job_roles,
1315
}.compact_blank!
1416
end
1517
end

app/views/publishers/vacancies/index.html.slim

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
vacancies: @vacancies, count: @count,
1111
vacancy_types: @vacancy_types,
1212
selected_organisation_ids: @selected_organisation_ids,
13+
selected_job_roles: @selected_job_roles,
1314
filter_form: @filter_form)
1415

1516
= govuk_pagination(pagy: @pagy)

config/locales/forms.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -257,6 +257,9 @@ en:
257257
edit_schools_html: "Can't see the school you are looking for? %{link}"
258258
publishers_job_listing_subjects_form:
259259
subjects_placeholder: Search
260+
publishers_vacancy_filter_form:
261+
organisation_ids_placeholder: Search locations
262+
job_roles_placeholder: Search job roles
260263
publishers_job_listing_job_title_form:
261264
job_title:
262265
education_support: For example ‘Learning support assistant’

config/locales/jobs.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ en:
99

1010
dashboard:
1111
add_or_remove_schools: Add or remove schools
12+
location_filter_hint: You can add or remove schools that are in this list
1213
awaiting_feedback:
1314
tab_heading: Jobs awaiting feedback
1415
with_count: Awaiting feedback (%{count})

spec/components/dashboard_component_spec.rb

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,16 @@
77
let(:publisher_preference) { create(:publisher_preference, publisher: publisher, organisation: organisation) }
88

99
let(:selected_organisation_ids) { [] }
10-
let(:filter_form) { Publishers::VacancyFilterForm.new(organisation_ids: selected_organisation_ids) }
10+
let(:selected_job_roles) { [] }
11+
let(:filter_form) { Publishers::VacancyFilterForm.new(organisation_ids: selected_organisation_ids, job_roles: selected_job_roles) }
1112

1213
subject do
1314
described_class.new(
1415
organisation: organisation, sort: sort, selected_type: selected_type,
1516
publisher_preference: publisher_preference, vacancies: vacancies,
1617
count: vacancies.count, vacancy_types: %i[live draft pending expired awaiting_feedback],
1718
selected_organisation_ids: selected_organisation_ids,
19+
selected_job_roles: selected_job_roles,
1820
filter_form: filter_form
1921
)
2022
end

spec/form_models/publishers/vacancy_filter_form_spec.rb

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,20 +7,30 @@
77
expect(form.organisation_ids).to eq(%w[123 456])
88
end
99

10+
it "sets job_roles from params" do
11+
form = described_class.new(job_roles: %w[teacher headteacher])
12+
expect(form.job_roles).to eq(%w[teacher headteacher])
13+
end
14+
1015
it "defaults organisation_ids to empty array when not provided" do
1116
form = described_class.new
1217
expect(form.organisation_ids).to eq([])
1318
end
19+
20+
it "defaults job_roles to empty array when not provided" do
21+
form = described_class.new
22+
expect(form.job_roles).to eq([])
23+
end
1424
end
1525

1626
describe "#to_hash" do
17-
it "returns hash with organisation_ids" do
18-
form = described_class.new(organisation_ids: %w[123 456])
19-
expect(form.to_hash).to eq({ organisation_ids: %w[123 456] })
27+
it "returns hash with both filters" do
28+
form = described_class.new(organisation_ids: %w[123 456], job_roles: %w[teacher headteacher])
29+
expect(form.to_hash).to eq({ organisation_ids: %w[123 456], job_roles: %w[teacher headteacher] })
2030
end
2131

2232
it "removes blank values from hash" do
23-
form = described_class.new(organisation_ids: [])
33+
form = described_class.new(organisation_ids: [], job_roles: [])
2434
expect(form.to_hash).to eq({})
2535
end
2636
end

spec/system/publishers/publishers_can_filter_vacancies_in_their_dashboard_spec.rb

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,4 +147,38 @@
147147
expect(page).to_not have_content(school2_draft_vacancy.job_title)
148148
end
149149
end
150+
151+
context "when attempting to access unauthorised organisations via URL params" do
152+
let(:organisation) { trust }
153+
let(:unauthorised_school) { create(:school, name: "Unauthorised School") }
154+
let!(:unauthorised_vacancy) { create(:vacancy, organisations: [unauthorised_school], job_title: "Unauthorised Job") }
155+
156+
scenario "it prevents access to vacancies from unauthorised organisations" do
157+
visit organisation_jobs_with_type_path(organisation_ids: [school1.id, unauthorised_school.id])
158+
159+
# Should see the authorized school's vacancy
160+
expect(page).to have_content(school1_vacancy.job_title)
161+
162+
# Should NOT see the unauthorised school's vacancy
163+
expect(page).to_not have_content(unauthorised_vacancy.job_title)
164+
165+
expect(page).to have_css(".filters-component__remove-tags__tag", count: 1)
166+
expect(page).to have_content("Happy Rainbows School")
167+
expect(page).to_not have_content("Unauthorised School")
168+
end
169+
end
170+
171+
context "when filtering by job roles" do
172+
let(:organisation) { trust }
173+
let!(:teacher_vacancy) { create(:vacancy, job_roles: %w[teacher], organisations: [trust], job_title: "Teacher Position") }
174+
let!(:headteacher_vacancy) { create(:vacancy, job_roles: %w[headteacher], organisations: [trust], job_title: "Headteacher Position") }
175+
176+
scenario "it filters vacancies by selected job role" do
177+
visit organisation_jobs_with_type_path(job_roles: %w[teacher])
178+
179+
expect(page).to have_content(teacher_vacancy.job_title)
180+
expect(page).to_not have_content(headteacher_vacancy.job_title)
181+
expect(page).to have_css(".filters-component__remove-tags__tag", count: 1)
182+
end
183+
end
150184
end

0 commit comments

Comments
 (0)