bright 0.1.0 → 1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +5 -5
- data/LICENSE.txt +1 -1
- data/bright.gemspec +7 -5
- data/lib/bright/address.rb +6 -8
- data/lib/bright/connection.rb +52 -44
- data/lib/bright/contact.rb +53 -0
- data/lib/bright/cursor_response_collection.rb +45 -0
- data/lib/bright/email_address.rb +7 -0
- data/lib/bright/errors.rb +13 -3
- data/lib/bright/helpers/blank_helper.rb +55 -0
- data/lib/bright/helpers/boolean_parser_helper.rb +13 -0
- data/lib/bright/phone_number.rb +20 -0
- data/lib/bright/response_collection.rb +20 -19
- data/lib/bright/school.rb +13 -4
- data/lib/bright/sis_apis/aeries.rb +36 -35
- data/lib/bright/sis_apis/base.rb +31 -5
- data/lib/bright/sis_apis/bright_sis.rb +305 -0
- data/lib/bright/sis_apis/infinite_campus.rb +376 -18
- data/lib/bright/sis_apis/power_school.rb +112 -69
- data/lib/bright/sis_apis/skyward.rb +276 -0
- data/lib/bright/sis_apis/synergy.rb +2 -2
- data/lib/bright/sis_apis/tsis.rb +44 -42
- data/lib/bright/student.rb +57 -14
- data/lib/bright/version.rb +1 -1
- data/lib/bright.rb +28 -14
- metadata +50 -15
@@ -1,31 +1,389 @@
|
|
1
|
+
require 'oauth'
|
2
|
+
|
1
3
|
module Bright
|
2
4
|
module SisApi
|
3
5
|
class InfiniteCampus < Base
|
4
|
-
|
5
|
-
|
6
|
-
|
6
|
+
|
7
|
+
@@description = "Connects to the Infinite Campus OneRoster API for accessing student information"
|
8
|
+
@@doc_url = "https://content.infinitecampus.com/sis/latest/documentation/oneroster-api"
|
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
|
+
# }
|
7
31
|
end
|
8
|
-
|
9
|
-
def
|
10
|
-
|
32
|
+
|
33
|
+
def api_version
|
34
|
+
Gem::Version.new(self.connection_options.dig(:api_version) || @@api_version)
|
11
35
|
end
|
12
|
-
|
13
|
-
def
|
14
|
-
|
36
|
+
|
37
|
+
def get_student_by_api_id(api_id, params = {})
|
38
|
+
if api_version <= Gem::Version.new("1.1")
|
39
|
+
params = {:role => "student"}.merge(params)
|
40
|
+
else
|
41
|
+
params = {:roles => "student"}.merge(params)
|
42
|
+
end
|
43
|
+
st_hsh = self.request(:get, "users/#{api_id}", params)
|
44
|
+
Student.new(convert_to_user_data(st_hsh["user"])) if st_hsh and st_hsh["user"]
|
15
45
|
end
|
16
|
-
|
17
|
-
def
|
18
|
-
|
46
|
+
|
47
|
+
def get_student(params = {}, options = {})
|
48
|
+
self.get_students(params, options.merge(:limit => 1, :wrap_in_collection => false)).first
|
49
|
+
end
|
50
|
+
|
51
|
+
def get_students(params = {}, options = {})
|
52
|
+
if api_version <= Gem::Version.new("1.1")
|
53
|
+
params = {:role => "student"}.merge(params)
|
54
|
+
else
|
55
|
+
params = {:roles => "student"}.merge(params)
|
56
|
+
end
|
57
|
+
params[:limit] = params[:limit] || options[:limit] || 100
|
58
|
+
students_response_hash = self.request(:get, 'users', self.map_search_params(params))
|
59
|
+
total_results = students_response_hash[:response_headers]["x-total-count"].to_i
|
60
|
+
if students_response_hash and students_response_hash["users"]
|
61
|
+
students_hash = [students_response_hash["users"]].flatten
|
62
|
+
|
63
|
+
students = students_hash.compact.collect {|st_hsh|
|
64
|
+
Student.new(convert_to_user_data(st_hsh))
|
65
|
+
}
|
66
|
+
end
|
67
|
+
if options[:wrap_in_collection] != false
|
68
|
+
api = self
|
69
|
+
load_more_call = proc { |page|
|
70
|
+
# pages start at one, so add a page here
|
71
|
+
params[:offset] = (params[:limit].to_i * page)
|
72
|
+
api.get_students(params, {:wrap_in_collection => false})
|
73
|
+
}
|
74
|
+
ResponseCollection.new({
|
75
|
+
:seed_page => students,
|
76
|
+
:total => total_results,
|
77
|
+
:per_page => params[:limit],
|
78
|
+
:load_more_call => load_more_call,
|
79
|
+
:no_threads => options[:no_threads]
|
80
|
+
})
|
81
|
+
else
|
82
|
+
students
|
83
|
+
end
|
19
84
|
end
|
20
|
-
|
21
|
-
def
|
85
|
+
|
86
|
+
def create_student(student)
|
22
87
|
raise NotImplementedError
|
23
88
|
end
|
24
|
-
|
25
|
-
def
|
89
|
+
|
90
|
+
def update_student(student)
|
26
91
|
raise NotImplementedError
|
27
92
|
end
|
28
|
-
|
93
|
+
|
94
|
+
def get_school_by_api_id(api_id, params = {})
|
95
|
+
sc_hsh = self.request(:get, "schools/#{api_id}", params)
|
96
|
+
School.new(convert_to_school_data(sc_hsh["org"])) if sc_hsh and sc_hsh["org"]
|
97
|
+
end
|
98
|
+
|
99
|
+
def get_school(params = {}, options = {})
|
100
|
+
self.get_schools(params, options.merge(:limit => 1, :wrap_in_collection => false)).first
|
101
|
+
end
|
102
|
+
|
103
|
+
def get_schools(params = {}, options = {})
|
104
|
+
params[:limit] = params[:limit] || options[:limit] || 100
|
105
|
+
schools_response_hash = self.request(:get, 'schools', self.map_school_search_params(params))
|
106
|
+
total_results = schools_response_hash[:response_headers]["x-total-count"].to_i
|
107
|
+
if schools_response_hash and schools_response_hash["orgs"]
|
108
|
+
schools_hash = [schools_response_hash["orgs"]].flatten
|
109
|
+
|
110
|
+
schools = schools_hash.compact.collect {|sc_hsh|
|
111
|
+
School.new(convert_to_school_data(sc_hsh))
|
112
|
+
}
|
113
|
+
end
|
114
|
+
if options[:wrap_in_collection] != false
|
115
|
+
api = self
|
116
|
+
load_more_call = proc { |page|
|
117
|
+
# pages start at one, so add a page here
|
118
|
+
params[:offset] = (params[:limit].to_i * page)
|
119
|
+
api.get_schools(params, {:wrap_in_collection => false})
|
120
|
+
}
|
121
|
+
ResponseCollection.new({
|
122
|
+
:seed_page => schools,
|
123
|
+
:total => total_results,
|
124
|
+
:per_page => params[:limit],
|
125
|
+
:load_more_call => load_more_call,
|
126
|
+
:no_threads => options[:no_threads]
|
127
|
+
})
|
128
|
+
else
|
129
|
+
schools
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
def get_contact_by_api_id(api_id, params ={})
|
134
|
+
contact_hsh = self.request(:get, "users/#{api_id}", params)
|
135
|
+
Contact.new(convert_to_user_data(contact_hsh["user"], bright_type: "Contact")) if contact_hsh and contact_hsh["user"]
|
136
|
+
end
|
137
|
+
|
138
|
+
def request(method, path, params = {})
|
139
|
+
uri = "#{self.connection_options[:uri]}/#{path}"
|
140
|
+
body = nil
|
141
|
+
if method == :get
|
142
|
+
query = URI.encode_www_form(params)
|
143
|
+
uri += "?#{query}" unless query.strip == ""
|
144
|
+
else
|
145
|
+
body = JSON.dump(params)
|
146
|
+
end
|
147
|
+
|
148
|
+
response = connection_retry_wrapper {
|
149
|
+
connection = Bright::Connection.new(uri)
|
150
|
+
headers = self.headers_for_auth(uri)
|
151
|
+
connection.request(method, body, headers)
|
152
|
+
}
|
153
|
+
|
154
|
+
if !response.error?
|
155
|
+
response_hash = JSON.parse(response.body)
|
156
|
+
response_hash[:response_headers] = response.headers
|
157
|
+
else
|
158
|
+
puts "#{response.inspect}"
|
159
|
+
puts "#{response.body}"
|
160
|
+
end
|
161
|
+
response_hash
|
162
|
+
end
|
163
|
+
|
164
|
+
protected
|
165
|
+
|
166
|
+
def headers_for_auth(uri)
|
167
|
+
case api_version
|
168
|
+
when Gem::Version.new("1.1")
|
169
|
+
site = URI.parse(self.connection_options[:uri])
|
170
|
+
site = "#{site.scheme}://#{site.host}"
|
171
|
+
consumer = OAuth::Consumer.new(self.connection_options[:client_id], self.connection_options[:client_secret], { :site => site, :scheme => :header })
|
172
|
+
options = {:timestamp => Time.now.to_i, :nonce => SecureRandom.uuid}
|
173
|
+
{"Authorization" => consumer.create_signed_request(:get, uri, nil, options)["Authorization"]}
|
174
|
+
when Gem::Version.new("1.2")
|
175
|
+
if self.connection_options[:access_token].nil? or self.connection_options[:access_token_expires] < Time.now
|
176
|
+
self.retrieve_access_token
|
177
|
+
end
|
178
|
+
{
|
179
|
+
"Authorization" => "Bearer #{self.connection_options[:access_token]}",
|
180
|
+
"Accept" => "application/json;charset=UTF-8",
|
181
|
+
"Content-Type" =>"application/json;charset=UTF-8"
|
182
|
+
}
|
183
|
+
end
|
184
|
+
end
|
185
|
+
|
186
|
+
def retrieve_access_token
|
187
|
+
connection = Bright::Connection.new(self.connection_options[:token_uri])
|
188
|
+
response = connection.request(:post,
|
189
|
+
{
|
190
|
+
"grant_type" => "client_credentials",
|
191
|
+
"username" => self.connection_options[:client_id],
|
192
|
+
"password" => self.connection_options[:client_secret]
|
193
|
+
},
|
194
|
+
self.headers_for_access_token
|
195
|
+
)
|
196
|
+
if !response.error?
|
197
|
+
response_hash = JSON.parse(response.body)
|
198
|
+
end
|
199
|
+
if response_hash["access_token"]
|
200
|
+
self.connection_options[:access_token] = response_hash["access_token"]
|
201
|
+
self.connection_options[:access_token_expires] = (Time.now - 10) + response_hash["expires_in"]
|
202
|
+
end
|
203
|
+
response_hash
|
204
|
+
end
|
205
|
+
|
206
|
+
def headers_for_access_token
|
207
|
+
{
|
208
|
+
"Authorization" => "Basic #{Base64.strict_encode64("#{self.connection_options[:client_id]}:#{self.connection_options[:client_secret]}")}",
|
209
|
+
"Content-Type" => "application/x-www-form-urlencoded;charset=UTF-8"
|
210
|
+
}
|
211
|
+
end
|
212
|
+
|
213
|
+
def map_search_params(params)
|
214
|
+
params = params.dup
|
215
|
+
default_params = {}
|
216
|
+
|
217
|
+
filter = []
|
218
|
+
params.each do |k,v|
|
219
|
+
case k.to_s
|
220
|
+
when "first_name"
|
221
|
+
filter << "givenName='#{v}'"
|
222
|
+
when "last_name"
|
223
|
+
filter << "familyName='#{v}'"
|
224
|
+
when "email"
|
225
|
+
filter << "email='#{v}'"
|
226
|
+
when "student_id"
|
227
|
+
filter << "identifier='#{v}'"
|
228
|
+
when "last_modified"
|
229
|
+
filter << "dateLastModified>='#{v.to_time.utc.xmlschema}'"
|
230
|
+
when "role"
|
231
|
+
filter << "role='#{v}'"
|
232
|
+
else
|
233
|
+
default_params[k] = v
|
234
|
+
end
|
235
|
+
end
|
236
|
+
unless filter.empty?
|
237
|
+
params = {"filter" => filter.join(" AND ")}
|
238
|
+
end
|
239
|
+
default_params.merge(params).reject{|k,v| v.respond_to?(:empty?) ? v.empty? : v.nil?}
|
240
|
+
end
|
241
|
+
|
242
|
+
def map_school_search_params(params)
|
243
|
+
params = params.dup
|
244
|
+
default_params = {}
|
245
|
+
filter = []
|
246
|
+
params.each do |k,v|
|
247
|
+
case k.to_s
|
248
|
+
when "number"
|
249
|
+
filter << "identifier='#{v}'"
|
250
|
+
when "last_modified"
|
251
|
+
filter << "dateLastModified>='#{v.to_time.utc.xmlschema}'"
|
252
|
+
else
|
253
|
+
default_params[k] = v
|
254
|
+
end
|
255
|
+
end
|
256
|
+
unless filter.empty?
|
257
|
+
params = {"filter" => filter.join(" AND ")}
|
258
|
+
end
|
259
|
+
default_params.merge(params).reject{|k,v| v.respond_to?(:empty?) ? v.empty? : v.nil?}
|
260
|
+
end
|
261
|
+
|
262
|
+
def convert_to_school_data(school_params)
|
263
|
+
return {} if school_params.blank?
|
264
|
+
school_data_hsh = {
|
265
|
+
:api_id => school_params["sourcedId"],
|
266
|
+
:name => school_params["name"],
|
267
|
+
:number => school_params["identifier"],
|
268
|
+
:last_modified => school_params["dateLastModified"]
|
269
|
+
}
|
270
|
+
return school_data_hsh
|
271
|
+
end
|
272
|
+
|
273
|
+
def convert_to_user_data(user_params, bright_type: "Student")
|
274
|
+
return {} if user_params.blank?
|
275
|
+
user_data_hsh = {
|
276
|
+
:api_id => user_params["sourcedId"],
|
277
|
+
:first_name => user_params["givenName"],
|
278
|
+
:middle_name => user_params["middleName"],
|
279
|
+
:last_name => user_params["familyName"],
|
280
|
+
:last_modified => user_params["dateLastModified"]
|
281
|
+
}.reject{|k,v| v.blank?}
|
282
|
+
unless user_params["identifier"].blank?
|
283
|
+
user_data_hsh[:sis_student_id] = user_params["identifier"]
|
284
|
+
end
|
285
|
+
unless user_params["userMasterIdentifier"].blank?
|
286
|
+
user_data_hsh[:state_student_id] = user_params["userMasterIdentifier"]
|
287
|
+
end
|
288
|
+
unless user_params["userIds"].blank?
|
289
|
+
if (state_id_hsh = user_params["userIds"].detect{|user_id_hsh| user_id_hsh["type"] == "stateID"})
|
290
|
+
user_data_hsh[:state_student_id] = state_id_hsh["identifier"]
|
291
|
+
end
|
292
|
+
end
|
293
|
+
unless user_params["email"].blank?
|
294
|
+
user_data_hsh[:email_address] = {
|
295
|
+
:email_address => user_params["email"]
|
296
|
+
}
|
297
|
+
end
|
298
|
+
unless user_params["orgs"].blank?
|
299
|
+
if (s = user_params["orgs"].detect{|org| org["href"] =~ /\/schools\//})
|
300
|
+
self.schools_cache ||= {}
|
301
|
+
if (attending_school = self.schools_cache[s["sourcedId"]]).nil?
|
302
|
+
attending_school = self.get_school_by_api_id(s["sourcedId"])
|
303
|
+
self.schools_cache[attending_school.api_id] = attending_school
|
304
|
+
end
|
305
|
+
end
|
306
|
+
if attending_school
|
307
|
+
user_data_hsh[:school] = attending_school
|
308
|
+
end
|
309
|
+
end
|
310
|
+
unless user_params["phone"].blank?
|
311
|
+
user_data_hsh[:phone_numbers] = [{:phone_number => user_params["phone"]}]
|
312
|
+
end
|
313
|
+
unless user_params["sms"].blank?
|
314
|
+
user_data_hsh[:phone_numbers] ||= []
|
315
|
+
user_data_hsh[:phone_numbers] << {:phone_number => user_params["sms"]}
|
316
|
+
end
|
317
|
+
|
318
|
+
#add the demographic information
|
319
|
+
demographics_hash = get_demographic_information(user_data_hsh[:api_id])
|
320
|
+
user_data_hsh.merge!(demographics_hash) unless demographics_hash.blank?
|
321
|
+
|
322
|
+
#if you're a student, build the contacts too
|
323
|
+
if bright_type == "Student" and !user_params["agents"].blank?
|
324
|
+
user_data_hsh[:contacts] = user_params["agents"].collect do |agent_hsh|
|
325
|
+
begin
|
326
|
+
self.get_contact_by_api_id(agent_hsh["sourcedId"])
|
327
|
+
rescue Bright::ResponseError => e
|
328
|
+
if !e.message.to_s.include?("404")
|
329
|
+
raise e
|
330
|
+
end
|
331
|
+
end
|
332
|
+
end.compact
|
333
|
+
user_data_hsh[:grade] = (user_params["grades"] || []).first
|
334
|
+
if !user_data_hsh[:grade].blank?
|
335
|
+
user_data_hsh[:grade_school_year] = get_grade_school_year
|
336
|
+
end
|
337
|
+
end
|
338
|
+
|
339
|
+
return user_data_hsh
|
340
|
+
end
|
341
|
+
|
342
|
+
def get_demographic_information(api_id)
|
343
|
+
demographic_hsh = {}
|
344
|
+
|
345
|
+
begin
|
346
|
+
demographics_params = request(:get, "demographics/#{api_id}")["demographics"]
|
347
|
+
rescue Bright::ResponseError => e
|
348
|
+
if e.message.to_s.include?('404')
|
349
|
+
return demographic_hsh
|
350
|
+
else
|
351
|
+
raise e
|
352
|
+
end
|
353
|
+
end
|
354
|
+
|
355
|
+
unless (bday = demographics_params["birthdate"] || demographics_params["birthDate"]).blank?
|
356
|
+
demographic_hsh[:birth_date] = Date.parse(bday).to_s
|
357
|
+
end
|
358
|
+
unless demographics_params["sex"].to_s[0].blank?
|
359
|
+
demographic_hsh[:gender] = demographics_params["sex"].to_s[0].upcase
|
360
|
+
end
|
361
|
+
DEMOGRAPHICS_CONVERSION.each do |demographics_key, demographics_value|
|
362
|
+
if demographics_params[demographics_key].to_bool
|
363
|
+
if demographics_value == "Hispanic Or Latino"
|
364
|
+
demographic_hsh[:hispanic_ethnicity] = true
|
365
|
+
else
|
366
|
+
demographic_hsh[:race] ||= []
|
367
|
+
demographic_hsh[:race] << demographics_value
|
368
|
+
end
|
369
|
+
end
|
370
|
+
end
|
371
|
+
return demographic_hsh
|
372
|
+
end
|
373
|
+
|
374
|
+
def get_grade_school_year(date = Date.today)
|
375
|
+
#return the school year of a specific date
|
376
|
+
self.school_years_cache ||= {}
|
377
|
+
if self.school_years_cache[date].nil?
|
378
|
+
academic_periods_params = self.request(:get, "academicSessions", {"filter" => "startDate<='#{date.to_s}' AND endDate>='#{date.to_s}' AND status='active'"})["academicSessions"]
|
379
|
+
school_years = academic_periods_params.map{|ap| ap["schoolYear"]}.uniq
|
380
|
+
if school_years.size == 1
|
381
|
+
self.school_years_cache[date] = school_years.first
|
382
|
+
end
|
383
|
+
end
|
384
|
+
return self.school_years_cache[date]
|
385
|
+
end
|
386
|
+
|
29
387
|
end
|
30
388
|
end
|
31
|
-
end
|
389
|
+
end
|