-
Notifications
You must be signed in to change notification settings - Fork 22
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
6dba22c
commit f92a59f
Showing
6 changed files
with
338 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
128 changes: 128 additions & 0 deletions
128
app/vacancy_sources/vacancy_source/source/vacancy_poster.rb
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,128 @@ | ||
require "cgi" | ||
|
||
class VacancySource::Source::VacancyPoster | ||
FEED_URL = ENV.fetch("VACANCY_SOURCE_VACANCY_POSTER_FEED_URL").freeze | ||
VENTRUS_TRUST_UID = "".freeze | ||
SOURCE_NAME = "vacancy_poster".freeze | ||
|
||
class FeedItem | ||
def initialize(xml_node) | ||
@xml_node = xml_node | ||
end | ||
|
||
def [](key) | ||
@xml_node.xpath(key)&.text&.presence | ||
end | ||
end | ||
|
||
include Enumerable | ||
|
||
def self.source_name | ||
SOURCE_NAME | ||
end | ||
|
||
def each | ||
items.each do |item| | ||
v = Vacancy.find_or_initialize_by( | ||
external_source: SOURCE_NAME, | ||
external_reference: item["reference"], | ||
) | ||
|
||
# An external vacancy is by definition always published | ||
v.status = :published | ||
# Consider publish_on date to be the first time we saw this vacancy come through | ||
# (i.e. today, unless it already has a publish on date set) | ||
v.publish_on ||= Date.today | ||
|
||
begin | ||
v.assign_attributes(attributes_for(item)) | ||
rescue ArgumentError => e | ||
v.errors.add(:base, e) | ||
end | ||
|
||
yield v | ||
end | ||
end | ||
|
||
def attributes_for(item) | ||
{ | ||
job_title: item["jobTitle"], | ||
job_advert: job_advert_for(item), | ||
salary: item["salary"], | ||
expires_at: Time.zone.parse(item["expiresAt"]), | ||
external_advert_url: item["advertUrl"], | ||
|
||
job_roles: job_roles_for(item), | ||
ect_status: ect_status_for(item), | ||
key_stages: item["keyStages"].presence&.split(","), | ||
subjects: item["subjects"].presence&.split(","), | ||
working_patterns: item["workingPatterns"].presence&.split(","), | ||
contract_type: item["contractType"].presence, | ||
phases: phase_for(item), | ||
visa_sponsorship_available: false, | ||
}.merge(organisation_fields(item)) | ||
end | ||
|
||
def job_advert_for(item) | ||
sanitised_text = Rails::Html::WhiteListSanitizer.new.sanitize(item["jobAdvert"], tags: %w[p br]) | ||
CGI.unescapeHTML(sanitised_text) | ||
end | ||
|
||
def ect_status_for(item) | ||
return unless item["ectSuitable"].presence | ||
|
||
item["ectSuitable"] == "yes" ? "ect_suitable" : "ect_unsuitable" | ||
end | ||
|
||
def organisation_fields(item) | ||
schools = find_schools(item) | ||
first_school = schools.first | ||
|
||
{ | ||
organisations: schools, | ||
readable_job_location: first_school&.name, | ||
about_school: first_school&.description, | ||
} | ||
end | ||
|
||
def find_schools(item) | ||
multi_academy_trust = SchoolGroup.trusts.find_by(uid: item["trustUID"]) | ||
|
||
multi_academy_trust&.schools&.where(urn: item["schoolUrns"]).presence || | ||
Organisation.where(urn: item["schoolUrns"]).presence || | ||
Array(multi_academy_trust) | ||
end | ||
|
||
def job_roles_for(item) | ||
role = item["jobRole"]&.strip | ||
return [] if role.blank? | ||
|
||
# Translate legacy senior/middle leader into all the granular roles split from them | ||
return Vacancy::SENIOR_LEADER_JOB_ROLES if role.include? "senior_leader" | ||
return Vacancy::MIDDLE_LEADER_JOB_ROLES if role.include? "middle_leader" | ||
|
||
Array.wrap(role.gsub("deputy_headteacher_principal", "deputy_headteacher") | ||
.gsub("assistant_headteacher_principal", "assistant_headteacher") | ||
.gsub("headteacher_principal", "headteacher") | ||
.gsub(/head_of_year_or_phase|head_of_year/, "head_of_year_or_phase") | ||
.gsub(/learning_support|other_support|science_technician/, "education_support") | ||
.gsub(/\s+/, "")) | ||
end | ||
|
||
def phase_for(item) | ||
return if item["phase"].blank? | ||
|
||
item["phase"].strip | ||
.parameterize(separator: "_") | ||
.gsub("through_school", "through") | ||
.gsub(/16-19|16_19/, "sixth_form_or_college") | ||
end | ||
|
||
def items | ||
feed.xpath("//item").map { |fi| FeedItem.new(fi) } | ||
end | ||
|
||
def feed | ||
@feed ||= Nokogiri::XML(HTTParty.get(FEED_URL).body) | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
<?xml version="1.0" encoding="UTF-8"?> | ||
<channel> | ||
<title>ATS Integration RSS Feed</title> | ||
<link>https://example.com</link> | ||
<description>Ats integration RSS Feed</description> | ||
<item> | ||
<reference><![CDATA[coventrycc/TP/123/1234]]></reference> | ||
<advertUrl><![CDATA[https://jobs.test.gov.uk/members/?j=1234]]></adverturl> | ||
<publishOn>2023-06-06 00:00:00</publishOn> | ||
<expiresAt>2023-07-05 00:00:00</expiresAt> | ||
<jobTitle><![CDATA[Teacher (SEN) - Castle Wood School]]></jobTitle> | ||
<jobAdvert><![CDATA[<b>What is the job role?</b><br/><p>Castle Wood is an outstanding special school.</em></strong></p>]]></jobAdvert> | ||
<salary><![CDATA[TMS and UPS with SEN allowance]]></salary> | ||
<additionalAllowances><![CDATA[TMS and UPS with SEN allowance]]></additionalAllowances> | ||
<startDate>2023-06-06 00:00:00</startDate> | ||
<schoolUrns><![CDATA[111111]]></schoolUrns> | ||
<trustUID><![CDATA[]]></trustUID> | ||
<jobRole><![CDATA[sendco]]></jobRole> | ||
<ectSuitable><![CDATA[false]]></ectSuitable> | ||
<workingPatterns><![CDATA[part_time]]></workingPatterns> | ||
<contractType><![CDATA[fixed_term]]></contractType> | ||
<phase><![CDATA[primary]]></phase> | ||
<keyStages><![CDATA[ks3]]></keyStages> | ||
<subjects><![CDATA[]]></subjects> | ||
</item> | ||
</channel> |
181 changes: 181 additions & 0 deletions
181
spec/vacancy_sources/vacancy_source/source/vancancy_poster_spec.rb
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,181 @@ | ||
require "rails_helper" | ||
|
||
RSpec.describe VacancySource::Source::VacancyPoster do | ||
let(:response_body) { file_fixture("vacancy_sources/vacancy_poster.xml").read } | ||
let(:response) { double("VacancyPosterHttpResponse", success?: true, body: response_body) } | ||
|
||
let!(:school1) { create(:school, name: "Test School", urn: "111111", phase: :primary) } | ||
let(:schools) { [school1] } | ||
|
||
describe "enumeration" do | ||
let(:vacancy) { subject.first } | ||
let(:job_role) { "sendco" } | ||
|
||
let(:expected_vacancy) do | ||
{ | ||
job_title: "Teacher (SEN) - Castle Wood School", | ||
job_advert: "<b>What is the job role?</b><br/><p>Castle Wood is an outstanding special school.</em></strong></p>", | ||
salary: "TMS and UPS with SEN allowance", | ||
job_roles: [job_role], | ||
key_stages: ["ks3"], | ||
working_patterns: %w[part_time], | ||
contract_type: "fixed_term", | ||
phases: %w[primary], | ||
visa_sponsorship_available: false, | ||
} | ||
end | ||
|
||
before do | ||
expect(HTTParty).to receive(:get).with("http://example.com/feed.xml").and_return(response) | ||
end | ||
|
||
it "has the correct number of vacancies" do | ||
expect(subject.count).to eq(1) | ||
end | ||
|
||
it "yields vacancies with correct attributes" do | ||
expect { |b| subject.each(&b) }.to yield_with_args(an_instance_of(Vacancy)) | ||
end | ||
|
||
it "yield a newly built vacancy the correct vacancy information" do | ||
expect(vacancy).not_to be_persisted | ||
expect(vacancy).to be_changed | ||
end | ||
|
||
it "assigns correct attributes from the feed" do | ||
expect(vacancy).to have_attributes(expected_vacancy) | ||
end | ||
|
||
it "assigns the vacancy to the correct school and organisation" do | ||
expect(vacancy.organisations.first).to eq(school1) | ||
|
||
expect(vacancy.external_source).to eq("vacancy_poster") | ||
expect(vacancy.external_advert_url).to eq("https://jobs.test.gov.uk/members/?j=1234") | ||
expect(vacancy.external_reference).to eq("coventrycc/TP/123/1234") | ||
|
||
expect(vacancy.organisations).to eq(schools) | ||
end | ||
|
||
describe "job roles mapping" do | ||
let(:response_body) { super().gsub("sendco", job_role) } | ||
|
||
%w[deputy_headteacher_principal deputy_headteacher].each do |role| | ||
context "when the source role is '#{role}'" do | ||
let(:job_role) { role } | ||
|
||
it "maps the source role to '[deputy_headteacher]' in the vacancy" do | ||
expect(vacancy.job_roles).to eq(["deputy_headteacher"]) | ||
end | ||
end | ||
end | ||
|
||
%w[assistant_headteacher_principal assistant_headteacher].each do |role| | ||
context "when the source role is '#{role}'" do | ||
let(:job_role) { role } | ||
|
||
it "maps the source role to '[assistant_headteacher]' in the vacancy" do | ||
expect(vacancy.job_roles).to eq(["assistant_headteacher"]) | ||
end | ||
end | ||
end | ||
|
||
%w[headteacher_principal headteacher].each do |role| | ||
context "when the source role is '#{role}'" do | ||
let(:job_role) { role } | ||
|
||
it "maps the source role to '[headteacher]' in the vacancy" do | ||
expect(vacancy.job_roles).to eq(["headteacher"]) | ||
end | ||
end | ||
end | ||
|
||
context "when the source role is 'senior_leader'" do | ||
let(:job_role) { "senior_leader" } | ||
|
||
it "maps the source role to '[headteacher, assistant_headteacher, deputy_headteacher]' in the vacancy" do | ||
expect(vacancy.job_roles).to contain_exactly("headteacher", "assistant_headteacher", "deputy_headteacher") | ||
end | ||
end | ||
|
||
%w[head_of_year_or_phase head_of_year].each do |role| | ||
context "when the source role is '#{role}'" do | ||
let(:job_role) { role } | ||
|
||
it "maps the source role to '[head_of_year_or_phase]' in the vacancy" do | ||
expect(vacancy.job_roles).to eq(["head_of_year_or_phase"]) | ||
end | ||
end | ||
end | ||
|
||
context "when the source role is 'head_of_department_or_curriculum'" do | ||
let(:job_role) { "head_of_department_or_curriculum" } | ||
|
||
it "maps the source role to '[head_of_department_or_curriculum]' in the vacancy" do | ||
expect(vacancy.job_roles).to eq(["head_of_department_or_curriculum"]) | ||
end | ||
end | ||
|
||
context "when the source role is 'middle_leader'" do | ||
let(:job_role) { "middle_leader" } | ||
|
||
it "maps the source role to '[head_of_year_or_phase, head_of_department_or_curriculum]' in the vacancy" do | ||
expect(vacancy.job_roles).to contain_exactly("head_of_year_or_phase", "head_of_department_or_curriculum") | ||
end | ||
end | ||
|
||
%w[learning_support other_support science_technician].each do |role| | ||
context "when the source role is '#{role}'" do | ||
let(:job_role) { role } | ||
|
||
it "maps the source role to '[education_support]' in the vacancy" do | ||
expect(vacancy.job_roles).to eq(["education_support"]) | ||
end | ||
end | ||
end | ||
end | ||
|
||
describe "phase mapping" do | ||
let(:response_body) { super().gsub("primary", phase) } | ||
|
||
%w[16-19 16_19].each do |phase| | ||
context "when the phase is '#{phase}'" do | ||
let(:phase) { phase } | ||
|
||
it "maps the phase to '[sixth_form_or_college]' in the vacancy" do | ||
expect(vacancy.phases).to eq(["sixth_form_or_college"]) | ||
end | ||
end | ||
end | ||
|
||
context "when the phase is 'through_school'" do | ||
let(:phase) { "through_school" } | ||
|
||
it "maps the phase to '[through]' in the vacancy" do | ||
expect(vacancy.phases).to eq(["through"]) | ||
end | ||
end | ||
end | ||
|
||
context "when the same vacancy has been imported previously" do | ||
let!(:existing_vacancy) do | ||
create( | ||
:vacancy, | ||
:external, | ||
phases: %w[primary], | ||
external_source: "vacancy_poster", | ||
external_reference: "coventrycc/TP/123/1234", | ||
organisations: schools, | ||
job_title: "Out of date", | ||
) | ||
end | ||
|
||
it "yields the existing vacancy with updated information" do | ||
expect(vacancy.id).to eq(existing_vacancy.id) | ||
expect(vacancy).to be_persisted | ||
expect(vacancy).to be_changed | ||
|
||
expect(vacancy.job_title).to eq("Teacher (SEN) - Castle Wood School") | ||
end | ||
end | ||
end | ||
end |