Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Build iTrent integration #6476

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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
3 changes: 2 additions & 1 deletion app/jobs/import_from_vacancy_source_job.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ class ImportFromVacancySourceJob < ApplicationJob
queue_as :low

def perform(source_klass)
return if DisableExpensiveJobs.enabled?
# UNCOMMENT BEFORE MERGING
# return if DisableExpensiveJobs.enabled?

@source_klass = source_klass
@source_name = source_klass.source_name
Expand Down
185 changes: 185 additions & 0 deletions app/vacancy_sources/vacancy_source/source/itrent.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
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: item.dig("vacancyptysrchs", "vacancyptysrch", "urllinks", "urllink", "link"),
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 results
feed.dig("RequisitionData", "requisition")
end

def feed
response = HTTParty.get(FEED_URL, headers: FEED_HEADERS, basic_auth: AUTH, verify: false)
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, verify: false)
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" : [
]
}
}