bright 1.1 → 1.2.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/bright.gemspec +1 -1
- data/lib/bright/sis_apis/base.rb +1 -1
- data/lib/bright/sis_apis/focus.rb +377 -0
- data/lib/bright/sis_apis/skyward.rb +1 -1
- data/lib/bright/version.rb +1 -1
- data/lib/bright.rb +1 -0
- metadata +4 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: ed9189d57c0583da9fe8c388390b5fdfe7d1219ec1aa7f705c8d5bdcb86a380d
|
4
|
+
data.tar.gz: 114e30a3f065134cd467aeb5186662cc0b296dff3b35ea276d790661bfc3ed58
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: bcf315c80eb63fa3c4b27e92255721f92d79ed1bc1593cddb75c635e59b8d77d01ddf4e95a6dd4184524e174c2a5f9e2ad310f66c3228fad779bd8c94d8de73b
|
7
|
+
data.tar.gz: 764e099470d73800e6ee90fdbc8cd38e20aac97e65226a8caee377d306f959870637e7e7526bbc54bcd7ff644d3e343a580c805ef299a3cfbcf88d968c696b4d
|
data/bright.gemspec
CHANGED
@@ -10,7 +10,7 @@ Gem::Specification.new do |spec|
|
|
10
10
|
spec.email = ["hello@arux.software"]
|
11
11
|
spec.summary = "Framework and tools for dealing with Student Information Systems"
|
12
12
|
spec.description = "Bright is a simple Student Information System API abstraction library used in and sponsored by Arux Software. It is written by Stephen Heuer, Steven Novotny, and contributors. The aim of the project is to abstract as many parts as possible away from the user to offer a consistent interface across all supported Student Information System APIs."
|
13
|
-
spec.homepage = ""
|
13
|
+
spec.homepage = "https://github.com/Arux-Software/Bright"
|
14
14
|
spec.license = "MIT"
|
15
15
|
|
16
16
|
spec.files = `git ls-files -z`.split("\x0")
|
data/lib/bright/sis_apis/base.rb
CHANGED
@@ -35,7 +35,7 @@ module Bright
|
|
35
35
|
else
|
36
36
|
raise
|
37
37
|
end
|
38
|
-
rescue Errno::ECONNREFUSED, Errno::ECONNRESET, Net::ReadTimeout, Net::OpenTimeout, EOFError => e
|
38
|
+
rescue Errno::ECONNREFUSED, Errno::ECONNRESET, Net::ReadTimeout, Net::OpenTimeout, SocketError, EOFError => e
|
39
39
|
retries += 1
|
40
40
|
if retries <= retry_attempts.to_i
|
41
41
|
puts "retrying #{retries}: #{e.class.to_s} - #{e.to_s}"
|
@@ -0,0 +1,377 @@
|
|
1
|
+
require 'oauth'
|
2
|
+
|
3
|
+
module Bright
|
4
|
+
module SisApi
|
5
|
+
class Focus < Base
|
6
|
+
|
7
|
+
@@description = "Connects to the Focus OneRoster API for accessing student information"
|
8
|
+
@@doc_url = ""
|
9
|
+
@@api_version = "1.1"
|
10
|
+
|
11
|
+
attr_accessor :connection_options, :schools_cache, :school_years_cache
|
12
|
+
|
13
|
+
DEMOGRAPHICS_CONVERSION = {
|
14
|
+
"americanIndianOrAlaskaNative"=>"American Indian Or Alaska Native",
|
15
|
+
"asian"=>"Asian",
|
16
|
+
"blackOrAfricanAmerican"=>"Black Or African American",
|
17
|
+
"nativeHawaiianOrOtherPacificIslander"=>"Native Hawaiian Or Other Pacific Islander",
|
18
|
+
"white"=>"White",
|
19
|
+
"hispanicOrLatinoEthnicity"=>"Hispanic Or Latino"
|
20
|
+
}
|
21
|
+
|
22
|
+
def initialize(options = {})
|
23
|
+
self.connection_options = options[:connection] || {}
|
24
|
+
# {
|
25
|
+
# :client_id => "",
|
26
|
+
# :client_secret => "",
|
27
|
+
# :api_version => "", (defaults to @@api_version)
|
28
|
+
# :uri => "",
|
29
|
+
# :token_uri => "" (api_version 1.2 required)
|
30
|
+
# }
|
31
|
+
end
|
32
|
+
|
33
|
+
def api_version
|
34
|
+
Gem::Version.new(self.connection_options.dig(:api_version) || @@api_version)
|
35
|
+
end
|
36
|
+
|
37
|
+
def get_student_by_api_id(api_id, params = {})
|
38
|
+
st_hsh = self.request(:get, "students/#{api_id}", params)
|
39
|
+
Student.new(convert_to_user_data(st_hsh["user"])) if st_hsh and st_hsh["user"]
|
40
|
+
end
|
41
|
+
|
42
|
+
def get_student(params = {}, options = {})
|
43
|
+
self.get_students(params, options.merge(:limit => 1, :wrap_in_collection => false)).first
|
44
|
+
end
|
45
|
+
|
46
|
+
def get_students(params = {}, options = {})
|
47
|
+
params[:limit] = params[:limit] || options[:limit] || 100
|
48
|
+
students_response_hash = self.request(:get, 'students', self.map_search_params(params))
|
49
|
+
total_results = students_response_hash[:response_headers]["x-total-count"].to_i
|
50
|
+
if students_response_hash and students_response_hash["users"]
|
51
|
+
students_hash = [students_response_hash["users"]].flatten
|
52
|
+
|
53
|
+
students = students_hash.compact.collect {|st_hsh|
|
54
|
+
Student.new(convert_to_user_data(st_hsh))
|
55
|
+
}
|
56
|
+
end
|
57
|
+
if options[:wrap_in_collection] != false
|
58
|
+
api = self
|
59
|
+
load_more_call = proc { |page|
|
60
|
+
# pages start at one, so add a page here
|
61
|
+
params[:offset] = (params[:limit].to_i * page)
|
62
|
+
api.get_students(params, {:wrap_in_collection => false})
|
63
|
+
}
|
64
|
+
ResponseCollection.new({
|
65
|
+
:seed_page => students,
|
66
|
+
:total => total_results,
|
67
|
+
:per_page => params[:limit],
|
68
|
+
:load_more_call => load_more_call,
|
69
|
+
:no_threads => options[:no_threads]
|
70
|
+
})
|
71
|
+
else
|
72
|
+
students
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def create_student(student)
|
77
|
+
raise NotImplementedError
|
78
|
+
end
|
79
|
+
|
80
|
+
def update_student(student)
|
81
|
+
raise NotImplementedError
|
82
|
+
end
|
83
|
+
|
84
|
+
def get_school_by_api_id(api_id, params = {})
|
85
|
+
sc_hsh = self.request(:get, "schools/#{api_id}", params)
|
86
|
+
School.new(convert_to_school_data(sc_hsh["org"])) if sc_hsh and sc_hsh["org"]
|
87
|
+
end
|
88
|
+
|
89
|
+
def get_school(params = {}, options = {})
|
90
|
+
self.get_schools(params, options.merge(:limit => 1, :wrap_in_collection => false)).first
|
91
|
+
end
|
92
|
+
|
93
|
+
def get_schools(params = {}, options = {})
|
94
|
+
params[:limit] = params[:limit] || options[:limit] || 100
|
95
|
+
schools_response_hash = self.request(:get, 'schools', self.map_school_search_params(params))
|
96
|
+
total_results = schools_response_hash[:response_headers]["x-total-count"].to_i
|
97
|
+
if schools_response_hash and schools_response_hash["orgs"]
|
98
|
+
schools_hash = [schools_response_hash["orgs"]].flatten
|
99
|
+
|
100
|
+
schools = schools_hash.compact.collect {|sc_hsh|
|
101
|
+
School.new(convert_to_school_data(sc_hsh))
|
102
|
+
}
|
103
|
+
end
|
104
|
+
if options[:wrap_in_collection] != false
|
105
|
+
api = self
|
106
|
+
load_more_call = proc { |page|
|
107
|
+
# pages start at one, so add a page here
|
108
|
+
params[:offset] = (params[:limit].to_i * page)
|
109
|
+
api.get_schools(params, {:wrap_in_collection => false})
|
110
|
+
}
|
111
|
+
ResponseCollection.new({
|
112
|
+
:seed_page => schools,
|
113
|
+
:total => total_results,
|
114
|
+
:per_page => params[:limit],
|
115
|
+
:load_more_call => load_more_call,
|
116
|
+
:no_threads => options[:no_threads]
|
117
|
+
})
|
118
|
+
else
|
119
|
+
schools
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
def get_contact_by_api_id(api_id, params ={})
|
124
|
+
contact_hsh = self.request(:get, "users/#{api_id}", params)
|
125
|
+
Contact.new(convert_to_user_data(contact_hsh["user"], bright_type: "Contact")) if contact_hsh and contact_hsh["user"]
|
126
|
+
end
|
127
|
+
|
128
|
+
def request(method, path, params = {})
|
129
|
+
uri = "#{self.connection_options[:uri]}/#{path}"
|
130
|
+
body = nil
|
131
|
+
if method == :get
|
132
|
+
query = URI.encode_www_form(params)
|
133
|
+
uri += "?#{query}" unless query.strip == ""
|
134
|
+
else
|
135
|
+
body = JSON.dump(params)
|
136
|
+
end
|
137
|
+
|
138
|
+
response = connection_retry_wrapper {
|
139
|
+
connection = Bright::Connection.new(uri)
|
140
|
+
headers = self.headers_for_auth(uri)
|
141
|
+
connection.request(method, body, headers)
|
142
|
+
}
|
143
|
+
|
144
|
+
if !response.error?
|
145
|
+
response_hash = JSON.parse(response.body)
|
146
|
+
response_hash[:response_headers] = response.headers
|
147
|
+
else
|
148
|
+
puts "#{response.inspect}"
|
149
|
+
puts "#{response.body}"
|
150
|
+
end
|
151
|
+
response_hash
|
152
|
+
end
|
153
|
+
|
154
|
+
protected
|
155
|
+
|
156
|
+
def headers_for_auth(uri)
|
157
|
+
case api_version
|
158
|
+
when Gem::Version.new("1.1")
|
159
|
+
site = URI.parse(self.connection_options[:uri])
|
160
|
+
site = "#{site.scheme}://#{site.host}"
|
161
|
+
consumer = OAuth::Consumer.new(self.connection_options[:client_id], self.connection_options[:client_secret], { :site => site, :scheme => :header })
|
162
|
+
options = {:timestamp => Time.now.to_i, :nonce => SecureRandom.uuid}
|
163
|
+
{"Authorization" => consumer.create_signed_request(:get, uri, nil, options)["Authorization"]}
|
164
|
+
when Gem::Version.new("1.2")
|
165
|
+
if self.connection_options[:access_token].nil? or self.connection_options[:access_token_expires] < Time.now
|
166
|
+
self.retrieve_access_token
|
167
|
+
end
|
168
|
+
{
|
169
|
+
"Authorization" => "Bearer #{self.connection_options[:access_token]}",
|
170
|
+
"Accept" => "application/json;charset=UTF-8",
|
171
|
+
"Content-Type" =>"application/json;charset=UTF-8"
|
172
|
+
}
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
def retrieve_access_token
|
177
|
+
connection = Bright::Connection.new(self.connection_options[:token_uri])
|
178
|
+
response = connection.request(:post,
|
179
|
+
{
|
180
|
+
"grant_type" => "client_credentials",
|
181
|
+
"username" => self.connection_options[:client_id],
|
182
|
+
"password" => self.connection_options[:client_secret]
|
183
|
+
},
|
184
|
+
self.headers_for_access_token
|
185
|
+
)
|
186
|
+
if !response.error?
|
187
|
+
response_hash = JSON.parse(response.body)
|
188
|
+
end
|
189
|
+
if response_hash["access_token"]
|
190
|
+
self.connection_options[:access_token] = response_hash["access_token"]
|
191
|
+
self.connection_options[:access_token_expires] = (Time.now - 10) + response_hash["expires_in"]
|
192
|
+
end
|
193
|
+
response_hash
|
194
|
+
end
|
195
|
+
|
196
|
+
def headers_for_access_token
|
197
|
+
{
|
198
|
+
"Authorization" => "Basic #{Base64.strict_encode64("#{self.connection_options[:client_id]}:#{self.connection_options[:client_secret]}")}",
|
199
|
+
"Content-Type" => "application/x-www-form-urlencoded;charset=UTF-8"
|
200
|
+
}
|
201
|
+
end
|
202
|
+
|
203
|
+
def map_search_params(params)
|
204
|
+
params = params.dup
|
205
|
+
default_params = {}
|
206
|
+
|
207
|
+
filter = []
|
208
|
+
params.each do |k,v|
|
209
|
+
case k.to_s
|
210
|
+
when "first_name"
|
211
|
+
filter << "givenName='#{v}'"
|
212
|
+
when "last_name"
|
213
|
+
filter << "familyName='#{v}'"
|
214
|
+
when "email"
|
215
|
+
filter << "email='#{v}'"
|
216
|
+
when "student_id"
|
217
|
+
filter << "identifier='#{v}'"
|
218
|
+
when "last_modified"
|
219
|
+
filter << "dateLastModified>='#{v.to_time.utc.xmlschema}'"
|
220
|
+
when "role"
|
221
|
+
filter << "role='#{v}'"
|
222
|
+
else
|
223
|
+
default_params[k] = v
|
224
|
+
end
|
225
|
+
end
|
226
|
+
unless filter.empty?
|
227
|
+
params = {"filter" => filter.join(" AND ")}
|
228
|
+
end
|
229
|
+
default_params.merge(params).reject{|k,v| v.respond_to?(:empty?) ? v.empty? : v.nil?}
|
230
|
+
end
|
231
|
+
|
232
|
+
def map_school_search_params(params)
|
233
|
+
params = params.dup
|
234
|
+
default_params = {}
|
235
|
+
filter = []
|
236
|
+
params.each do |k,v|
|
237
|
+
case k.to_s
|
238
|
+
when "number"
|
239
|
+
filter << "identifier='#{v}'"
|
240
|
+
when "last_modified"
|
241
|
+
filter << "dateLastModified>='#{v.to_time.utc.xmlschema}'"
|
242
|
+
else
|
243
|
+
default_params[k] = v
|
244
|
+
end
|
245
|
+
end
|
246
|
+
unless filter.empty?
|
247
|
+
params = {"filter" => filter.join(" AND ")}
|
248
|
+
end
|
249
|
+
default_params.merge(params).reject{|k,v| v.respond_to?(:empty?) ? v.empty? : v.nil?}
|
250
|
+
end
|
251
|
+
|
252
|
+
def convert_to_school_data(school_params)
|
253
|
+
return {} if school_params.blank?
|
254
|
+
school_data_hsh = {
|
255
|
+
:api_id => school_params["sourcedId"],
|
256
|
+
:name => school_params["name"],
|
257
|
+
:number => school_params["identifier"],
|
258
|
+
:last_modified => school_params["dateLastModified"]
|
259
|
+
}
|
260
|
+
return school_data_hsh
|
261
|
+
end
|
262
|
+
|
263
|
+
def convert_to_user_data(user_params, bright_type: "Student")
|
264
|
+
return {} if user_params.blank?
|
265
|
+
user_data_hsh = {
|
266
|
+
:api_id => user_params["sourcedId"],
|
267
|
+
:first_name => user_params["givenName"],
|
268
|
+
:middle_name => user_params["middleName"],
|
269
|
+
:last_name => user_params["familyName"],
|
270
|
+
:last_modified => user_params["dateLastModified"]
|
271
|
+
}.reject{|k,v| v.blank?}
|
272
|
+
unless user_params["identifier"].blank?
|
273
|
+
user_data_hsh[:sis_student_id] = user_params["identifier"]
|
274
|
+
end
|
275
|
+
unless user_params["userMasterIdentifier"].blank?
|
276
|
+
user_data_hsh[:state_student_id] = user_params["userMasterIdentifier"]
|
277
|
+
end
|
278
|
+
unless user_params.dig("metadata", "stateId").blank?
|
279
|
+
user_data_hsh[:state_student_id] = user_params.dig("metadata", "stateId")
|
280
|
+
end
|
281
|
+
unless user_params["email"].blank?
|
282
|
+
user_data_hsh[:email_address] = {
|
283
|
+
:email_address => user_params["email"]
|
284
|
+
}
|
285
|
+
end
|
286
|
+
unless user_params["orgs"].blank?
|
287
|
+
if (s = user_params["orgs"].detect{|org| org["href"] =~ /\/schools\//})
|
288
|
+
self.schools_cache ||= {}
|
289
|
+
if (attending_school = self.schools_cache[s["sourcedId"]]).nil?
|
290
|
+
attending_school = self.get_school_by_api_id(s["sourcedId"])
|
291
|
+
self.schools_cache[attending_school.api_id] = attending_school
|
292
|
+
end
|
293
|
+
end
|
294
|
+
if attending_school
|
295
|
+
user_data_hsh[:school] = attending_school
|
296
|
+
end
|
297
|
+
end
|
298
|
+
unless user_params["phone"].blank?
|
299
|
+
user_data_hsh[:phone_numbers] = [{:phone_number => user_params["phone"]}]
|
300
|
+
end
|
301
|
+
unless user_params["sms"].blank?
|
302
|
+
user_data_hsh[:phone_numbers] ||= []
|
303
|
+
user_data_hsh[:phone_numbers] << {:phone_number => user_params["sms"]}
|
304
|
+
end
|
305
|
+
|
306
|
+
#add the demographic information
|
307
|
+
demographics_hash = get_demographic_information(user_data_hsh[:api_id])
|
308
|
+
user_data_hsh.merge!(demographics_hash) unless demographics_hash.blank?
|
309
|
+
|
310
|
+
#if you're a student, build the contacts too
|
311
|
+
if bright_type == "Student" and !user_params["agents"].blank?
|
312
|
+
user_data_hsh[:contacts] = user_params["agents"].collect do |agent_hsh|
|
313
|
+
begin
|
314
|
+
self.get_contact_by_api_id(agent_hsh["sourcedId"])
|
315
|
+
rescue Bright::ResponseError => e
|
316
|
+
if !e.message.to_s.include?("404")
|
317
|
+
raise e
|
318
|
+
end
|
319
|
+
end
|
320
|
+
end.compact
|
321
|
+
user_data_hsh[:grade] = (user_params["grades"] || []).first
|
322
|
+
if !user_data_hsh[:grade].blank?
|
323
|
+
user_data_hsh[:grade_school_year] = get_grade_school_year
|
324
|
+
end
|
325
|
+
end
|
326
|
+
|
327
|
+
return user_data_hsh
|
328
|
+
end
|
329
|
+
|
330
|
+
def get_demographic_information(api_id)
|
331
|
+
demographic_hsh = {}
|
332
|
+
|
333
|
+
begin
|
334
|
+
demographics_params = request(:get, "demographics/#{api_id}")["demographics"]
|
335
|
+
rescue Bright::ResponseError => e
|
336
|
+
if e.message.to_s.include?('404')
|
337
|
+
return demographic_hsh
|
338
|
+
else
|
339
|
+
raise e
|
340
|
+
end
|
341
|
+
end
|
342
|
+
|
343
|
+
unless (bday = demographics_params["birthdate"] || demographics_params["birthDate"]).blank?
|
344
|
+
demographic_hsh[:birth_date] = Date.parse(bday).to_s
|
345
|
+
end
|
346
|
+
unless demographics_params["sex"].to_s[0].blank?
|
347
|
+
demographic_hsh[:gender] = demographics_params["sex"].to_s[0].upcase
|
348
|
+
end
|
349
|
+
DEMOGRAPHICS_CONVERSION.each do |demographics_key, demographics_value|
|
350
|
+
if demographics_params[demographics_key].to_bool
|
351
|
+
if demographics_value == "Hispanic Or Latino"
|
352
|
+
demographic_hsh[:hispanic_ethnicity] = true
|
353
|
+
else
|
354
|
+
demographic_hsh[:race] ||= []
|
355
|
+
demographic_hsh[:race] << demographics_value
|
356
|
+
end
|
357
|
+
end
|
358
|
+
end
|
359
|
+
return demographic_hsh
|
360
|
+
end
|
361
|
+
|
362
|
+
def get_grade_school_year(date = Date.today)
|
363
|
+
# return the school year of a specific date
|
364
|
+
self.school_years_cache ||= {}
|
365
|
+
if self.school_years_cache[date].nil?
|
366
|
+
academic_periods_params = self.request(:get, "academicSessions", {"filter" => "startDate<='#{date.to_s}' AND endDate>='#{date.to_s}' AND status='active'"})["academicSessions"]
|
367
|
+
school_years = academic_periods_params.map{|ap| ap["schoolYear"]}.uniq
|
368
|
+
if school_years.size == 1
|
369
|
+
self.school_years_cache[date] = school_years.first
|
370
|
+
end
|
371
|
+
end
|
372
|
+
return self.school_years_cache[date]
|
373
|
+
end
|
374
|
+
|
375
|
+
end
|
376
|
+
end
|
377
|
+
end
|
@@ -214,7 +214,7 @@ module Bright
|
|
214
214
|
end
|
215
215
|
|
216
216
|
["PhoneNumber", "PhoneNumber2", "PhoneNumber3"].each do |phone_param|
|
217
|
-
if
|
217
|
+
if user_params[phone_param].present? && user_params["#{phone_param}Type"].present?
|
218
218
|
user_data_hsh[:phone_numbers] ||= []
|
219
219
|
user_data_hsh[:phone_numbers] << {
|
220
220
|
:phone_number => user_params[phone_param],
|
data/lib/bright/version.rb
CHANGED
data/lib/bright.rb
CHANGED
@@ -26,6 +26,7 @@ require_relative "bright/sis_apis/infinite_campus.rb"
|
|
26
26
|
require_relative "bright/sis_apis/skyward.rb"
|
27
27
|
require_relative "bright/sis_apis/bright_sis.rb"
|
28
28
|
require_relative "bright/sis_apis/synergy.rb"
|
29
|
+
require_relative "bright/sis_apis/focus.rb"
|
29
30
|
|
30
31
|
module Bright
|
31
32
|
class << self
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: bright
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 1.2.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Arux Software
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2022-
|
11
|
+
date: 2022-10-10 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: httpi
|
@@ -128,6 +128,7 @@ files:
|
|
128
128
|
- lib/bright/sis_apis/aeries.rb
|
129
129
|
- lib/bright/sis_apis/base.rb
|
130
130
|
- lib/bright/sis_apis/bright_sis.rb
|
131
|
+
- lib/bright/sis_apis/focus.rb
|
131
132
|
- lib/bright/sis_apis/infinite_campus.rb
|
132
133
|
- lib/bright/sis_apis/power_school.rb
|
133
134
|
- lib/bright/sis_apis/skyward.rb
|
@@ -135,7 +136,7 @@ files:
|
|
135
136
|
- lib/bright/sis_apis/tsis.rb
|
136
137
|
- lib/bright/student.rb
|
137
138
|
- lib/bright/version.rb
|
138
|
-
homepage:
|
139
|
+
homepage: https://github.com/Arux-Software/Bright
|
139
140
|
licenses:
|
140
141
|
- MIT
|
141
142
|
metadata: {}
|