bright 1.1 → 1.2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: dd99510122d136894a8e1ba1fdfb6bea0c424f355624ca2c68178143fcd7965d
4
- data.tar.gz: 16b8599ad55c91cf1f925a76da5d49690c49e2315ca63b925b71d31e421de45c
3
+ metadata.gz: c82ee5f6425fcb5502b799753f6c58748f13bba3cbb829c81c5b36f79986526e
4
+ data.tar.gz: e220dad592f23ba583f75135f4ced2aa2f4c47a8cfa9fea89b7d32bcbb819245
5
5
  SHA512:
6
- metadata.gz: e4499da391786116fef2ef77fbf283fd67f4c4f26f84277f2bf75825d9b2444712f7df8b7bb99ba3279f4c10612addbaaa47dda84ffda2f8e9b4240539d80939
7
- data.tar.gz: b544283e756c7114aac543d7b7a3c4d6acf87de7b5c58593a96bcafa619628444a7eaa91b0256dada1e28519914d381fc022563dce10814b5e8d0dbccca74736
6
+ metadata.gz: 835186a8b148d9133b474af00ace4d9edbdb8e9a82e216f9f987ea164e8de78488664d6a5fcd6f16bdded954cc423e6f8cd24662222eddeb51831127dc2c6362
7
+ data.tar.gz: 928d4a06cb9e335eabce0b18579909f000deb88aeba9ffd0d5433c0880fa848f5d586214f49f858d3cee90605905d9308a5d68e53e13644388706d5848a74523
@@ -35,7 +35,7 @@ module Bright
35
35
  else
36
36
  raise
37
37
  end
38
- rescue Errno::ECONNREFUSED, Errno::ECONNRESET, Net::ReadTimeout, Net::OpenTimeout, EOFError => e
38
+ rescue Errno::ECONNREFUSED, Errno::ECONNRESET, Net::ReadTimeout, Net::OpenTimeout, SocketError, EOFError => e
39
39
  retries += 1
40
40
  if retries <= retry_attempts.to_i
41
41
  puts "retrying #{retries}: #{e.class.to_s} - #{e.to_s}"
@@ -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
@@ -1,3 +1,3 @@
1
1
  module Bright
2
- VERSION = "1.1"
2
+ VERSION = "1.2"
3
3
  end
data/lib/bright.rb CHANGED
@@ -26,6 +26,7 @@ require_relative "bright/sis_apis/infinite_campus.rb"
26
26
  require_relative "bright/sis_apis/skyward.rb"
27
27
  require_relative "bright/sis_apis/bright_sis.rb"
28
28
  require_relative "bright/sis_apis/synergy.rb"
29
+ require_relative "bright/sis_apis/focus.rb"
29
30
 
30
31
  module Bright
31
32
  class << self
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: bright
3
3
  version: !ruby/object:Gem::Version
4
- version: '1.1'
4
+ version: '1.2'
5
5
  platform: ruby
6
6
  authors:
7
7
  - Arux Software
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2022-07-07 00:00:00.000000000 Z
11
+ date: 2022-08-09 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: httpi
@@ -128,6 +128,7 @@ files:
128
128
  - lib/bright/sis_apis/aeries.rb
129
129
  - lib/bright/sis_apis/base.rb
130
130
  - lib/bright/sis_apis/bright_sis.rb
131
+ - lib/bright/sis_apis/focus.rb
131
132
  - lib/bright/sis_apis/infinite_campus.rb
132
133
  - lib/bright/sis_apis/power_school.rb
133
134
  - lib/bright/sis_apis/skyward.rb