bright 0.1.0 → 1.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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