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.
@@ -1,31 +1,389 @@
1
+ require 'oauth'
2
+
1
3
  module Bright
2
4
  module SisApi
3
5
  class InfiniteCampus < Base
4
-
5
- def get_student_by_api_id(api_id)
6
- raise NotImplementedError
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 get_student(params)
10
- raise NotImplementedError
32
+
33
+ def api_version
34
+ Gem::Version.new(self.connection_options.dig(:api_version) || @@api_version)
11
35
  end
12
-
13
- def get_students(params)
14
- raise NotImplementedError
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 create_student(student, additional_params = {})
18
- raise NotImplementedError
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 update_student(student, additional_params = {})
85
+
86
+ def create_student(student)
22
87
  raise NotImplementedError
23
88
  end
24
-
25
- def get_schools(params)
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