bright 0.2.0 → 1.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,17 +1,17 @@
1
1
  module Bright
2
2
  module SisApi
3
3
  class Base
4
-
4
+
5
5
  def filter_students_by_params(students, params)
6
6
  total = params[:limit]
7
7
  count = 0
8
8
  found = []
9
-
9
+
10
10
  keys = (Student.attribute_names & params.keys.collect(&:to_sym))
11
11
  puts "filtering on #{keys.join(",")}"
12
12
  students.each do |student|
13
13
  break if total and count >= total
14
-
14
+
15
15
  should = (keys).all? do |m|
16
16
  student.send(m) =~ Regexp.new(Regexp.escape(params[m]), Regexp::IGNORECASE)
17
17
  end
@@ -20,7 +20,33 @@ module Bright
20
20
  end
21
21
  found
22
22
  end
23
-
23
+
24
+ def connection_retry_wrapper(&block)
25
+ retry_attempts = connection_options[:retry_attempts] || 2
26
+ retries = 0
27
+ begin
28
+ yield
29
+ rescue Bright::ResponseError => e
30
+ retries += 1
31
+ if e.server_error? && retries <= retry_attempts.to_i
32
+ puts "retrying #{retries}: #{e.class.to_s} - #{e.to_s}"
33
+ sleep(retries * 3)
34
+ retry
35
+ else
36
+ raise
37
+ end
38
+ rescue Errno::ECONNREFUSED, Errno::ECONNRESET, Net::ReadTimeout, Net::OpenTimeout, SocketError, EOFError => e
39
+ retries += 1
40
+ if retries <= retry_attempts.to_i
41
+ puts "retrying #{retries}: #{e.class.to_s} - #{e.to_s}"
42
+ sleep(retries * 3)
43
+ retry
44
+ else
45
+ raise
46
+ end
47
+ end
48
+ end
49
+
24
50
  end
25
51
  end
26
- end
52
+ end
@@ -55,7 +55,8 @@ module Bright
55
55
  :seed_page => students,
56
56
  :total => total_results,
57
57
  :per_page => params[:limit],
58
- :load_more_call => load_more_call
58
+ :load_more_call => load_more_call,
59
+ :no_threads => options[:no_threads]
59
60
  })
60
61
  else
61
62
  students
@@ -92,7 +93,8 @@ module Bright
92
93
  :seed_page => schools,
93
94
  :total => total_results,
94
95
  :per_page => params[:limit],
95
- :load_more_call => load_more_call
96
+ :load_more_call => load_more_call,
97
+ :no_threads => options[:no_threads]
96
98
  })
97
99
  else
98
100
  schools
@@ -108,11 +110,12 @@ module Bright
108
110
  else
109
111
  body = JSON.dump(params)
110
112
  end
111
- puts uri.inspect
112
- headers = self.headers_for_auth(uri)
113
113
 
114
- connection = Bright::Connection.new(uri)
115
- response = connection.request(method, body, headers)
114
+ response = connection_retry_wrapper {
115
+ connection = Bright::Connection.new(uri)
116
+ headers = self.headers_for_auth
117
+ connection.request(method, body, headers)
118
+ }
116
119
 
117
120
  if !response.error?
118
121
  response_hash = JSON.parse(response.body)
@@ -149,7 +152,6 @@ module Bright
149
152
 
150
153
  def convert_to_student_data(student_params)
151
154
  return {} if student_params.nil?
152
-
153
155
  student_data_hsh = {
154
156
  :api_id => student_params["uuid"],
155
157
  :first_name => student_params["first_name"],
@@ -165,7 +167,7 @@ module Bright
165
167
  :image => student_params["picture"],
166
168
  :hispanic_ethnicity => student_params["hispanic_latino"],
167
169
  :last_modified => student_params["updated_at"]
168
- }
170
+ }.reject{|k,v| v.blank?}
169
171
  unless student_params["birthdate"].blank?
170
172
  student_data_hsh[:birth_date] = Date.parse(student_params["birthdate"]).to_s
171
173
  end
@@ -177,19 +179,21 @@ module Bright
177
179
  end
178
180
  end
179
181
 
180
- unless student_params["student_street"].blank?
181
- student_data_hsh[:addresses] = [{
182
- :street => student_params["student_street"],
183
- :apt => student_params["student_street_line_2"],
184
- :city => student_params["student_city"],
185
- :state => student_params["student_state"],
186
- :postal_code => student_params["student_zip"]
187
- }]
182
+ unless student_params["addresses"].blank?
183
+ student_data_hsh[:addresses] = student_params["addresses"].collect do |address_params|
184
+ convert_to_address_data(address_params)
185
+ end
186
+ end
187
+
188
+ unless student_params["phone_numbers"].blank?
189
+ student_data_hsh[:phone_numbers] = student_params["phone_numbers"].collect do |phone_params|
190
+ convert_to_phone_number_data(phone_params)
191
+ end
188
192
  end
189
193
 
190
- unless student_params["student_email"].blank?
194
+ unless student_params["email_addresses"].blank?
191
195
  student_data_hsh[:email_address] = {
192
- :email_address => student_params["student_email"]
196
+ :email_address => student_params["email_addresses"].first["email_address"]
193
197
  }
194
198
  end
195
199
 
@@ -197,6 +201,36 @@ module Bright
197
201
  student_data_hsh[:school] = convert_to_school_data(student_params["school"])
198
202
  end
199
203
 
204
+ unless student_params["contacts"].blank?
205
+ student_data_hsh[:contacts] = student_params["contacts"].collect do |contact_params|
206
+ contact_data_hsh = {
207
+ :api_id => contact_params["uuid"],
208
+ :first_name => contact_params["first_name"],
209
+ :middle_name => contact_params["middle_name"],
210
+ :last_name => contact_params["last_name"],
211
+ :relationship_type => contact_params["relationship"],
212
+ :sis_student_id => contact_params["sis_id"],
213
+ :last_modified => contact_params["updated_at"]
214
+ }
215
+ unless contact_params["addresses"].blank?
216
+ contact_data_hsh[:addresses] = contact_params["addresses"].collect do |address_params|
217
+ convert_to_address_data(address_params)
218
+ end
219
+ end
220
+ unless contact_params["phone_numbers"].blank?
221
+ contact_data_hsh[:phone_numbers] = contact_params["phone_numbers"].collect do |phone_params|
222
+ convert_to_phone_number_data(phone_params)
223
+ end
224
+ end
225
+ unless contact_params["email_addresses"].blank?
226
+ contact_data_hsh[:email_address] = {
227
+ :email_address => contact_params["email_addresses"].first["email_address"]
228
+ }
229
+ end
230
+ contact_data_hsh.reject{|k,v| v.blank?}
231
+ end
232
+ end
233
+
200
234
  return student_data_hsh
201
235
  end
202
236
 
@@ -213,6 +247,28 @@ module Bright
213
247
  return filter_params
214
248
  end
215
249
 
250
+ def convert_to_phone_number_data(phone_number_params)
251
+ return {} if phone_number_params.nil?
252
+ {
253
+ :phone_number => phone_number_params["phone_number"],
254
+ :type => phone_number_params["phone_type"]
255
+ }.reject{|k,v| v.blank?}
256
+ end
257
+
258
+ def convert_to_address_data(address_params)
259
+ return {} if address_params.nil?
260
+ {
261
+ :street => address_params["street"],
262
+ :apt => address_params["street_line_2"],
263
+ :city => address_params["city"],
264
+ :state => address_params["state"],
265
+ :postal_code => address_params["zip"],
266
+ :lattitude => address_params["latitude"],
267
+ :longitude => address_params["longitude"],
268
+ :type => address_params["address_type"]
269
+ }.reject{|k,v| v.blank?}
270
+ end
271
+
216
272
  def convert_to_school_data(school_params)
217
273
  return {} if school_params.nil?
218
274
 
@@ -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