-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #793 from pulibrary/airtable
Basic functionality for airtable staff list
- Loading branch information
Showing
12 changed files
with
351 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
# frozen_string_literal: true | ||
|
||
module AirTableStaff | ||
# This class is responsible for creating a CSV out of the | ||
# data from Airtable | ||
class CSVBuilder | ||
def to_csv | ||
@csv ||= CSV.generate do |csv| | ||
# Add the headers... | ||
csv << StaffDirectoryMapping.new.to_a | ||
|
||
# Then add the data | ||
AirTableStaff::RecordList.new.to_a.each do |record| | ||
csv << record.to_a | ||
end | ||
end | ||
end | ||
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,22 @@ | ||
# frozen_string_literal: true | ||
module AirTableStaff | ||
# This class is responsible for extracting a single | ||
# value from a json hash, based on the criteria in | ||
# the field hash | ||
class JsonValueExtractor | ||
def initialize(json:, field:) | ||
@json = json | ||
@field = field | ||
end | ||
|
||
def extract | ||
raw_value = json[field[:airtable_field]] | ||
transformer = field[:transformer] | ||
transformer ? transformer.call(raw_value) : raw_value | ||
end | ||
|
||
private | ||
|
||
attr_reader :json, :field | ||
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,51 @@ | ||
# frozen_string_literal: true | ||
module AirTableStaff | ||
# This class is responsible for maintaining a list | ||
# of staff records taken from the Airtable API | ||
class RecordList | ||
def initialize | ||
@base_url = "https://api.airtable.com/v0/appv7XA5FWS7DG9oe/Synchronized%20Staff%20Directory%20View?view=Grid%20view" | ||
@token = LibJobs.config[:airtable_token] | ||
end | ||
|
||
# The library staff list is split into several pages. | ||
# For each page (except the last), Airtable gives us an | ||
# offset, which is how we request the next page. | ||
def to_a(offset: nil) | ||
@as_array ||= begin | ||
json = get_json(offset:) | ||
records = json[:records].map do |row| | ||
AirTableStaff::StaffDirectoryPerson.new(row[:fields]) | ||
end | ||
offset = json[:offset] | ||
|
||
# If we have an offset, call this method recursively | ||
# (to fetch additional pages of data), until airtable | ||
# no longer gives us an offset | ||
records += to_a(offset:) if offset | ||
records | ||
end | ||
end | ||
|
||
private | ||
|
||
attr_reader :base_url, :token | ||
|
||
def get_json(offset: nil) | ||
JSON.parse(response(offset:).body, symbolize_names: true) | ||
end | ||
|
||
def response(offset: nil) | ||
url = url_with_optional_offset(offset:) | ||
request = Net::HTTP::Get.new(url) | ||
request["Authorization"] = "Bearer #{token}" | ||
Net::HTTP.start(url.hostname, url.port, { use_ssl: true }) do |http| | ||
http.request(request) | ||
end | ||
end | ||
|
||
def url_with_optional_offset(offset: nil) | ||
URI.parse(offset ? "#{base_url}&offset=#{offset}" : base_url) | ||
end | ||
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,36 @@ | ||
# frozen_string_literal: true | ||
module AirTableStaff | ||
# This class is responsible for describing the fields in the | ||
# airtable and the resultant CSV, and how they are related | ||
class StaffDirectoryMapping | ||
# A mapping of: | ||
# - an airtable field (airtable_field) | ||
# - a column name we want for our csv (our_field) | ||
# - an optional "transformer" lambda for converting the airtable values | ||
# to the desired format for the csv | ||
# rubocop:disable Metrics/MethodLength | ||
def fields | ||
[ | ||
{ airtable_field: :'University ID', our_field: :puid }, | ||
{ airtable_field: :netid, our_field: :netid }, | ||
{ airtable_field: :'University Phone', our_field: :phone }, | ||
{ airtable_field: :'pul:Preferred Name', our_field: :name }, | ||
{ airtable_field: :'Last Name', our_field: :lastName }, | ||
{ airtable_field: :'First Name', our_field: :firstName }, | ||
{ airtable_field: :Email, our_field: :email }, | ||
{ airtable_field: :Address, our_field: :address }, | ||
{ airtable_field: :'pul:Building', our_field: :building }, | ||
{ airtable_field: :Division, our_field: :department }, | ||
{ airtable_field: :'pul:Department', our_field: :division }, | ||
{ airtable_field: :'pul:Unit', our_field: :unit }, | ||
{ airtable_field: :'pul:Team', our_field: :team }, | ||
{ airtable_field: :'Area of Study', our_field: :areasOfStudy, transformer: ->(areas) { areas&.join('//') } } | ||
] | ||
end | ||
# rubocop:enable Metrics/MethodLength | ||
|
||
def to_a | ||
@as_array ||= fields.pluck(:our_field) | ||
end | ||
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,22 @@ | ||
# frozen_string_literal: true | ||
module AirTableStaff | ||
# This class is responsible for extracting information about | ||
# a person from the airtable staff directory JSON, according | ||
# to the mapping from the StaffDirectoryMapping class. | ||
class StaffDirectoryPerson | ||
def initialize(json) | ||
@json = json | ||
@mapping = StaffDirectoryMapping.new | ||
end | ||
|
||
def to_a | ||
@array_version ||= mapping.fields.map do |field| | ||
JsonValueExtractor.new(field:, json:).extract | ||
end | ||
end | ||
|
||
private | ||
|
||
attr_reader :json, :mapping | ||
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
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,21 @@ | ||
{ | ||
"records": [ | ||
{ | ||
"id": "recrhIUhcJw3lUlRE", | ||
"createdTime": "2024-01-30T22:16:16.000Z", | ||
"fields": { | ||
"Address": "123 Stokes", | ||
"Area of Study": ["Virtual Reality"], | ||
"Division": "Stokes", | ||
"pul:Building": "Stokes", | ||
"Last Name": "Librarian", | ||
"First Name": "Phillip", | ||
"University ID": "123", | ||
"netid": "ab123", | ||
"University Phone": "(123) 123-1234", | ||
"pul:Preferred Name": "Phillip Librarian", | ||
"Email": "[email protected]" | ||
} | ||
} | ||
] | ||
} |
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,21 @@ | ||
{ | ||
"records": [ | ||
{ | ||
"id": "recrhIUhcJw3lUlRE", | ||
"createdTime": "2025-01-30T22:16:16.000Z", | ||
"fields": { | ||
"Address": "Firestone A floor", | ||
"Area of Study": ["Cinema history", "Robots"], | ||
"Division": "Special Collections", | ||
"pul:Building": "Firestone", | ||
"Last Name": "Carmant", | ||
"First Name": "Drema", | ||
"University ID": "456", | ||
"netid": "zz99", | ||
"University Phone": "(123) 555-5555", | ||
"pul:Preferred Name": "Drema Carmant", | ||
"Email": "[email protected]" | ||
} | ||
} | ||
], "offset": "naeQu2ul/Ash6eiQu" | ||
} |
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,22 @@ | ||
# frozen_string_literal: true | ||
require 'rails_helper' | ||
|
||
RSpec.describe AirTableStaff::CSVBuilder do | ||
before do | ||
stub_request(:get, "https://api.airtable.com/v0/appv7XA5FWS7DG9oe/Synchronized%20Staff%20Directory%20View?view=Grid%20view") | ||
.with( | ||
headers: { | ||
'Authorization' => 'Bearer FAKE_AIRTABLE_TOKEN' | ||
} | ||
) | ||
.to_return(status: 200, body: File.read(file_fixture('air_table/records_no_offset.json')), headers: {}) | ||
end | ||
it 'creates a CSV object with data from the HTTP API' do | ||
expected = <<~END_CSV | ||
puid,netid,phone,name,lastName,firstName,email,address,building,department,division,unit,team,areasOfStudy | ||
123,ab123,(123) 123-1234,Phillip Librarian,Librarian,Phillip,[email protected],123 Stokes,Stokes,Stokes,,,,Virtual Reality | ||
END_CSV | ||
directory = described_class.new | ||
expect(directory.to_csv).to eq(expected) | ||
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,19 @@ | ||
# frozen_string_literal: true | ||
require 'rails_helper' | ||
|
||
RSpec.describe AirTableStaff::JsonValueExtractor do | ||
context 'when there is no transformer lambda provided in field config' do | ||
it 'extracts the text verbatim' do | ||
json = { cat: 'tabby cat' } | ||
field = { airtable_field: :cat } | ||
expect(described_class.new(json:, field:).extract).to eq('tabby cat') | ||
end | ||
end | ||
context 'when there is a transformer lambda provided in field config' do | ||
it 'extracts the text verbatim' do | ||
json = { cat: 'tabby cat' } | ||
field = { airtable_field: :cat, transformer: ->(value) { "My favorite cat is #{value}" } } | ||
expect(described_class.new(json:, field:).extract).to eq('My favorite cat is tabby cat') | ||
end | ||
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,74 @@ | ||
# frozen_string_literal: true | ||
require 'rails_helper' | ||
|
||
BASE_AIRTABLE_URL = 'https://api.airtable.com/v0/appv7XA5FWS7DG9oe/Synchronized%20Staff%20Directory%20View?view=Grid%20view' | ||
|
||
RSpec.describe AirTableStaff::RecordList do | ||
context 'when the airtable response is not paginated' do | ||
before do | ||
stub_request(:get, BASE_AIRTABLE_URL) | ||
.with( | ||
headers: { | ||
'Authorization' => 'Bearer FAKE_AIRTABLE_TOKEN' | ||
} | ||
) | ||
.to_return(status: 200, body: File.read(file_fixture('air_table/records_no_offset.json')), headers: {}) | ||
end | ||
it 'creates an array with data from a single call to the HTTP API' do | ||
list = described_class.new.to_a | ||
|
||
expect(list.length).to eq(1) | ||
|
||
first_person = list[0].to_a | ||
expect(first_person[0]).to eq('123') # puid | ||
expect(first_person[3]).to eq('Phillip Librarian') # name | ||
|
||
expect(WebMock).to have_requested(:get, BASE_AIRTABLE_URL).once | ||
end | ||
end | ||
context 'when the airtable response is paginated' do | ||
before do | ||
stub_request(:get, BASE_AIRTABLE_URL) | ||
.with( | ||
headers: { | ||
'Authorization' => 'Bearer FAKE_AIRTABLE_TOKEN' | ||
} | ||
) | ||
.to_return(status: 200, body: File.read(file_fixture('air_table/records_with_offset.json')), headers: {}) | ||
stub_request(:get, "#{BASE_AIRTABLE_URL}&offset=naeQu2ul/Ash6eiQu") | ||
.with( | ||
headers: { | ||
'Authorization' => 'Bearer FAKE_AIRTABLE_TOKEN' | ||
} | ||
) | ||
.to_return(status: 200, body: File.read(file_fixture('air_table/records_no_offset.json')), headers: {}) | ||
end | ||
it 'creates an array with data from multiple calls to the HTTP API' do | ||
list = described_class.new.to_a | ||
|
||
expect(list.length).to eq(2) | ||
|
||
first_person = list[0].to_a | ||
expect(first_person[0]).to eq('456') # puid | ||
expect(first_person[3]).to eq('Drema Carmant') # name | ||
|
||
second_person = list[1].to_a | ||
expect(second_person[0]).to eq('123') # puid | ||
expect(second_person[3]).to eq('Phillip Librarian') # name | ||
|
||
expect(WebMock).to have_requested(:get, BASE_AIRTABLE_URL).once | ||
expect(WebMock).to have_requested(:get, "#{BASE_AIRTABLE_URL}&offset=naeQu2ul/Ash6eiQu").once | ||
end | ||
context 'when we run it multiple times' do | ||
it 'gives us the same data without additional network calls' do | ||
list = described_class.new | ||
first_time = list.to_a | ||
second_time = list.to_a | ||
|
||
expect(first_time).to eq(second_time) | ||
expect(WebMock).to have_requested(:get, BASE_AIRTABLE_URL).once | ||
expect(WebMock).to have_requested(:get, "#{BASE_AIRTABLE_URL}&offset=naeQu2ul/Ash6eiQu").once | ||
end | ||
end | ||
end | ||
end |
43 changes: 43 additions & 0 deletions
43
spec/models/air_table_staff/staff_directory_person_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,43 @@ | ||
# frozen_string_literal: true | ||
require 'rails_helper' | ||
|
||
RSpec.describe AirTableStaff::StaffDirectoryPerson do | ||
describe '#to_a' do | ||
it 'uses the order from the mapping' do | ||
json = { | ||
'pul:Preferred Name': 'Sage Archivist', | ||
'University ID': '987654321', | ||
'netid': 'ab1234', | ||
'University Phone': '(609) 555-1234', | ||
'Last Name': 'Archivist', | ||
'First Name': 'Phoenix', | ||
'Email': '[email protected]', | ||
'Address': '123 Lewis Library', | ||
'pul:Building': 'Stokes Library', | ||
'Division': 'ReCAP', | ||
'pul:Department': 'Cataloging and Metadata Services', | ||
'pul:Unit': 'Rare Books Cataloging Team', | ||
'pul:Team': 'IT, Discovery and Access Services', | ||
'Area of Study': ['Chemistry', 'African American Studies'] | ||
} | ||
expected = [ | ||
'987654321', # puid | ||
'ab1234', # netid | ||
'(609) 555-1234', # phone | ||
'Sage Archivist', # name | ||
'Archivist', # lastName | ||
'Phoenix', # firstName | ||
'[email protected]', # email | ||
'123 Lewis Library', # address | ||
'Stokes Library', # building | ||
'ReCAP', # department | ||
'Cataloging and Metadata Services', # division | ||
'Rare Books Cataloging Team', # unit | ||
'IT, Discovery and Access Services', # team | ||
'Chemistry//African American Studies' # areasOfStudy | ||
] | ||
|
||
expect(described_class.new(json).to_a).to eq(expected) | ||
end | ||
end | ||
end |