bright 0.2.0 → 1.2

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,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