Skip to content

Commit

Permalink
Add iTrent integration
Browse files Browse the repository at this point in the history
Build integration to pull Vacancies from iTrent API.
  • Loading branch information
scruti committed Mar 13, 2024
1 parent bafdc4a commit 8553601
Show file tree
Hide file tree
Showing 6 changed files with 685 additions and 0 deletions.
4 changes: 4 additions & 0 deletions .env.development
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,8 @@ VACANCY_SOURCE_EVERY_FEED_URL=http://example.com/feed.json
VACANCY_SOURCE_MY_NEW_TERM_FEED_URL=https://example.com
VACANCY_SOURCE_BROADBEAN_FEED_URL=http://example.com/feed.xml
VACANCY_SOURCE_MY_NEW_TERM_API_KEY=api_key
VACANCY_SOURCE_ITRENT_FEED_URL=http://example.com/feed.json
VACANCY_SOURCE_ITRENT_UDF_URL=http://example.com/udf.json
VACANCY_SOURCE_ITRENT_AUTH_USER=user
VACANCY_SOURCE_ITRENT_AUTH_PASSWORD=password
SECRET_KEY_BASE=352f2c2ccf5a9a77d8cc91fade3685dda45407f329c14ed0b8e4c2d6618d8475e4e4416bf20eeeec46558384ea9c9e2de97b85e20c47adb93d914576ed6b69d4
4 changes: 4 additions & 0 deletions .env.test
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ VACANCY_SOURCE_MY_NEW_TERM_FEED_URL=http://example.com
VACANCY_SOURCE_MY_NEW_TERM_API_KEY=test-api-key
VACANCY_SOURCE_BROADBEAN_FEED_URL=http://example.com/feed.xml
VACANCY_SOURCE_ARK_FEED_URL=http://example.com/feed.xml
VACANCY_SOURCE_ITRENT_FEED_URL=http://example.com/feed.json
VACANCY_SOURCE_ITRENT_UDF_URL=http://example.com/udf.json
VACANCY_SOURCE_ITRENT_AUTH_USER=user
VACANCY_SOURCE_ITRENT_AUTH_PASSWORD=password
BIGQUERY_TABLE_NAME=test_events
BIGQUERY_PROJECT_ID=teacher-vacancy-service
BIGQUERY_DATASET=testing_dataset
Expand Down
194 changes: 194 additions & 0 deletions app/vacancy_sources/vacancy_source/source/itrent.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
class VacancySource::Source::Itrent
class EveryImportError < StandardError; end

include HTTParty
include Enumerable
include VacancySource::Parser
include VacancySource::Shared

SOURCE_NAME = "itrent".freeze
ERROR_MESSAGE = "Something went wrong with iTrent Import. Response:".freeze
FEED_URL = ENV.fetch("VACANCY_SOURCE_ITRENT_FEED_URL").freeze
UDF_URL = ENV.fetch("VACANCY_SOURCE_ITRENT_UDF_URL").freeze

FEED_HEADERS = {
"Connection" => "keep-alive",
"iTrent-Operation" => "requisition",
"PartyName" => "MHR",
"content-type" => "application/json",
}.freeze

UDF_HEADERS = {
"Connection" => "keep-alive",
"iTrent-Operation" => "get_udfs",
"iTrent-Object-Type" => "REQUISITION",
"content-type" => "application/json",
"iTrent-UDF-ORG" => "MHR",
"iTrent-UDF-Category" => "TeachVacs",
}.freeze

AUTH = {
username: ENV.fetch("VACANCY_SOURCE_ITRENT_AUTH_USER"),
password: ENV.fetch("VACANCY_SOURCE_ITRENT_AUTH_PASSWORD"),
}.freeze

def self.source_name
SOURCE_NAME
end

def each
results.each do |result|
result["udfs"] = user_defined_fields(result["requisitionreference"])
schools = find_schools(result)
next if vacancy_listed_at_excluded_school_type?(schools)

v = Vacancy.find_or_initialize_by(
external_source: SOURCE_NAME,
external_reference: result["requisitionreference"],
)

# An external vacancy is by definition always published
v.status = :published

begin
v.assign_attributes(attributes_for(result, schools))
rescue ArgumentError => e
v.errors.add(:base, e)
end

yield v
end
end

private

def attributes_for(item, schools)
{
job_title: item["requisitionname"],
job_advert: item.dig("vacancyptysrchs", "vacancyptysrch", "jobdescription"),
salary: item.dig("vacancyptysrchs", "vacancyptysrch", "formattedsalarydescription"),
benefits_details: item.dig("udfs", "additionalallowances"),
expires_at: Time.zone.parse(item["applicationclosingdate"]),
external_advert_url: advert_url_for(item),
job_roles: job_roles_for(item),
ect_status: ect_status_for(item),
subjects: item.dig("udfs", "subjects").presence&.split("\n"),
working_patterns: item.dig("vacancyptysrchs", "vacancyptysrch", "basis").presence&.parameterize(separator: "_"),
contract_type: item.dig("udfs", "contracttype").presence&.parameterize(separator: "_"),
phases: phase_for(item),
key_stages: item.dig("udfs", "keystages").presence&.split(","),
visa_sponsorship_available: false,
publish_on: publish_on_for(item),
}.merge(organisation_fields(schools))
.merge(start_date_fields(item))
end

# 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)
def publish_on_for(item)
publish_on = item.dig("udfs", "publishon")
if publish_on.present?
Date.parse(publish_on)
else
Date.today
end
end

def start_date_fields(item)
return {} if item["startdate"].blank?

parsed_date = StartDate.new(item["startdate"])
if parsed_date.specific?
{ starts_on: parsed_date.date, start_date_type: parsed_date.type }
else
{ other_start_date_details: parsed_date.date, start_date_type: parsed_date.type }
end
end

def organisation_fields(schools)
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.dig("udfs", "trustuid"))
school_urns = item.dig("udfs", "schoolurns")&.split(",")

return [] if multi_academy_trust.blank? && school_urns.blank?
return Organisation.where(urn: school_urns) if multi_academy_trust.blank?
return Array(multi_academy_trust) if school_urns.blank?

# When having both trust and schools, only return the schools that are in the trust if any. Otherwise, return the trust itself.
multi_academy_trust.schools.where(urn: school_urns).order(:created_at).presence || Array(multi_academy_trust)
end

def multi_academy_trust(item)
SchoolGroup.trusts.find_by(uid: item.dig("udfs", "trustuid"))
end

def job_roles_for(item)
role = item.dig("udfs", "jobrole")&.strip&.downcase
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 ect_status_for(item)
item.dig("udfs", "ectsuitable").to_s == "true" ? "ect_suitable" : "ect_unsuitable"
end

def phase_for(item)
return if item.dig("udfs", "phase").blank?

item.dig("udfs", "phase")
.strip
.parameterize(separator: "_")
.gsub(/all_through|through_school/, "through")
.gsub(/16-19|16_19/, "sixth_form_or_college")
end

def advert_url_for(item)
url_link = item.dig("vacancyptysrchs", "vacancyptysrch", "urllinks", "urllink")
if url_link.is_a?(Array)
url_link.first&.fetch("link", nil)
else
url_link&.fetch("link", nil)
end
end

def results
feed.dig("RequisitionData", "requisition")
end

def feed
response = HTTParty.get(FEED_URL, headers: FEED_HEADERS, basic_auth: AUTH)
raise HTTParty::ResponseError, ERROR_MESSAGE unless response.success?

parsed_response = JSON.parse(response.body)
raise EveryImportError, ERROR_MESSAGE + parsed_response["error"] if parsed_response["error"]

parsed_response
end

def user_defined_fields(vacancy_ref)
headers = UDF_HEADERS.merge("iTrent-Object-Ref" => vacancy_ref)
response = HTTParty.get(UDF_URL, headers:, basic_auth: AUTH)
raise HTTParty::ResponseError, ERROR_MESSAGE unless response.success?

JSON.parse(response.body)&.dig("itrent", "udfs")&.first
end
end
45 changes: 45 additions & 0 deletions spec/fixtures/files/vacancy_sources/itrent_requisition.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
{
"RequisitionData": {
"requisition": [{
"requisitionreference": "FF00067",
"requisitionname": "Class Teacher",
"startdate": "2023-05-01",
"enddate": "",
"applicationclosingdate": "2023-06-30",
"recruitingmanager": "Mr Daniel Andrews He/They",
"requisitionadministrator": "Mrs Kathryn Baxendale She/Her",
"speculativeenquiries": "F",
"vacancyptysrchs": {
"vacancyptysrch": {
"requisitioncategorytypes": {
"requisitioncategorytype": "Human Resources"
},
"language": "USA",
"publish": "T",
"jobtitle": "HR Business Partner",
"jobdescription": "<p>Lorem Ipsum dolor sit amet</p>",
"salarysearchvalue": "24000",
"minimumsalary": "24000",
"maximumsalary": "30000",
"formattedsalarydescription": "£24,000 to £30,000 depending on working knowledge",
"package": "Company Pension, excellent employee benefits, 25 days holiday + bank holidays.",
"contractualhours": "37.00",
"basis": "Full time",
"region": "Central",
"location": "Birmingham",
"intranetonly": "F",
"keyword1": "HR",
"keyword2": "HUMAN RESOURCES",
"keyword3": "ASSISTANT",
"keyword4": "ADMINISTRATION",
"urllinks": {
"urllink": {
"webview": "MHR 2022 - 10.45",
"link": "https://teachvacs.itrentdemo.co.uk/ttrent_webrecruitment/wrd/run/ETREC179GF.open?WVID=2750500Wqk&VACANCY_ID=5116820Y9h"
}
}
}
}
}]
}
}
20 changes: 20 additions & 0 deletions spec/fixtures/files/vacancy_sources/itrent_udf.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"itrent" : {
"udfs" : [{
"category": "TeachVacs",
"udf" : "FF00083 - Maths Teacher",
"schoolurns" : "12345",
"trustuid" : "456789",
"jobrole" : "Teacher",
"contracttype" : "fixed term",
"phase" : "secondary",
"publishon" : "07\/08\/2023",
"additionalallowances" : "TLR2a",
"ectsuitable" : "true",
"keystages" : "ks2",
"subjects" : "maths\nadvanced maths"
}],
"messages" : [
]
}
}

0 comments on commit 8553601

Please sign in to comment.