bright 1.1 → 1.2.1

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: ed9189d57c0583da9fe8c388390b5fdfe7d1219ec1aa7f705c8d5bdcb86a380d
4
+ data.tar.gz: 114e30a3f065134cd467aeb5186662cc0b296dff3b35ea276d790661bfc3ed58
5
5
  SHA512:
6
- metadata.gz: e4499da391786116fef2ef77fbf283fd67f4c4f26f84277f2bf75825d9b2444712f7df8b7bb99ba3279f4c10612addbaaa47dda84ffda2f8e9b4240539d80939
7
- data.tar.gz: b544283e756c7114aac543d7b7a3c4d6acf87de7b5c58593a96bcafa619628444a7eaa91b0256dada1e28519914d381fc022563dce10814b5e8d0dbccca74736
6
+ metadata.gz: bcf315c80eb63fa3c4b27e92255721f92d79ed1bc1593cddb75c635e59b8d77d01ddf4e95a6dd4184524e174c2a5f9e2ad310f66c3228fad779bd8c94d8de73b
7
+ data.tar.gz: 764e099470d73800e6ee90fdbc8cd38e20aac97e65226a8caee377d306f959870637e7e7526bbc54bcd7ff644d3e343a580c805ef299a3cfbcf88d968c696b4d
data/bright.gemspec CHANGED
@@ -10,7 +10,7 @@ Gem::Specification.new do |spec|
10
10
  spec.email = ["hello@arux.software"]
11
11
  spec.summary = "Framework and tools for dealing with Student Information Systems"
12
12
  spec.description = "Bright is a simple Student Information System API abstraction library used in and sponsored by Arux Software. It is written by Stephen Heuer, Steven Novotny, and contributors. The aim of the project is to abstract as many parts as possible away from the user to offer a consistent interface across all supported Student Information System APIs."
13
- spec.homepage = ""
13
+ spec.homepage = "https://github.com/Arux-Software/Bright"
14
14
  spec.license = "MIT"
15
15
 
16
16
  spec.files = `git ls-files -z`.split("\x0")
@@ -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
@@ -214,7 +214,7 @@ module Bright
214
214
  end
215
215
 
216
216
  ["PhoneNumber", "PhoneNumber2", "PhoneNumber3"].each do |phone_param|
217
- if !user_params[phone_param].blank?
217
+ if user_params[phone_param].present? && user_params["#{phone_param}Type"].present?
218
218
  user_data_hsh[:phone_numbers] ||= []
219
219
  user_data_hsh[:phone_numbers] << {
220
220
  :phone_number => user_params[phone_param],
@@ -1,3 +1,3 @@
1
1
  module Bright
2
- VERSION = "1.1"
2
+ VERSION = "1.2.1"
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.1
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-10-10 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
@@ -135,7 +136,7 @@ files:
135
136
  - lib/bright/sis_apis/tsis.rb
136
137
  - lib/bright/student.rb
137
138
  - lib/bright/version.rb
138
- homepage: ''
139
+ homepage: https://github.com/Arux-Software/Bright
139
140
  licenses:
140
141
  - MIT
141
142
  metadata: {}