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.
@@ -5,10 +5,10 @@ module Bright
5
5
  class InfiniteCampus < Base
6
6
 
7
7
  @@description = "Connects to the Infinite Campus OneRoster API for accessing student information"
8
- @@doc_url = "https://content.infinitecampus.com/sis/Campus.1633/documentation/oneroster-api/"
9
- @@api_version = "v1.1"
8
+ @@doc_url = "https://content.infinitecampus.com/sis/latest/documentation/oneroster-api"
9
+ @@api_version = "1.1"
10
10
 
11
- attr_accessor :connection_options
11
+ attr_accessor :connection_options, :schools_cache, :school_years_cache
12
12
 
13
13
  DEMOGRAPHICS_CONVERSION = {
14
14
  "americanIndianOrAlaskaNative"=>"American Indian Or Alaska Native",
@@ -24,13 +24,24 @@ module Bright
24
24
  # {
25
25
  # :client_id => "",
26
26
  # :client_secret => "",
27
- # :uri => ""
27
+ # :api_version => "", (defaults to @@api_version)
28
+ # :uri => "",
29
+ # :token_uri => "" (api_version 1.2 required)
28
30
  # }
29
31
  end
30
32
 
33
+ def api_version
34
+ Gem::Version.new(self.connection_options.dig(:api_version) || @@api_version)
35
+ end
36
+
31
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
32
43
  st_hsh = self.request(:get, "users/#{api_id}", params)
33
- Student.new(convert_to_student_data(st_hsh["user"])) if st_hsh and st_hsh["user"]
44
+ Student.new(convert_to_user_data(st_hsh["user"])) if st_hsh and st_hsh["user"]
34
45
  end
35
46
 
36
47
  def get_student(params = {}, options = {})
@@ -38,14 +49,19 @@ module Bright
38
49
  end
39
50
 
40
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
41
57
  params[:limit] = params[:limit] || options[:limit] || 100
42
- students_response_hash = self.request(:get, 'users', self.map_student_search_params(params))
58
+ students_response_hash = self.request(:get, 'users', self.map_search_params(params))
43
59
  total_results = students_response_hash[:response_headers]["x-total-count"].to_i
44
60
  if students_response_hash and students_response_hash["users"]
45
61
  students_hash = [students_response_hash["users"]].flatten
46
62
 
47
63
  students = students_hash.compact.collect {|st_hsh|
48
- Student.new(convert_to_student_data(st_hsh))
64
+ Student.new(convert_to_user_data(st_hsh))
49
65
  }
50
66
  end
51
67
  if options[:wrap_in_collection] != false
@@ -59,7 +75,8 @@ module Bright
59
75
  :seed_page => students,
60
76
  :total => total_results,
61
77
  :per_page => params[:limit],
62
- :load_more_call => load_more_call
78
+ :load_more_call => load_more_call,
79
+ :no_threads => options[:no_threads]
63
80
  })
64
81
  else
65
82
  students
@@ -74,8 +91,48 @@ module Bright
74
91
  raise NotImplementedError
75
92
  end
76
93
 
77
- def get_schools(params)
78
- raise NotImplementedError
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"]
79
136
  end
80
137
 
81
138
  def request(method, path, params = {})
@@ -87,10 +144,12 @@ module Bright
87
144
  else
88
145
  body = JSON.dump(params)
89
146
  end
90
- headers = self.headers_for_auth(uri)
91
147
 
92
- connection = Bright::Connection.new(uri)
93
- response = connection.request(method, body, headers)
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
+ }
94
153
 
95
154
  if !response.error?
96
155
  response_hash = JSON.parse(response.body)
@@ -105,14 +164,53 @@ module Bright
105
164
  protected
106
165
 
107
166
  def headers_for_auth(uri)
108
- site = URI.parse(self.connection_options[:uri])
109
- site = "#{site.scheme}://#{site.host}"
110
- consumer = OAuth::Consumer.new(self.connection_options[:client_id], self.connection_options[:client_secret], { :site => site, :scheme => :header })
111
- options = {:timestamp => Time.now.to_i, :nonce => SecureRandom.uuid}
112
- {"Authorization" => consumer.create_signed_request(:get, uri, nil, options)["Authorization"]}
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
113
184
  end
114
185
 
115
- def map_student_search_params(params)
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)
116
214
  params = params.dup
117
215
  default_params = {}
118
216
 
@@ -127,6 +225,10 @@ module Bright
127
225
  filter << "email='#{v}'"
128
226
  when "student_id"
129
227
  filter << "identifier='#{v}'"
228
+ when "last_modified"
229
+ filter << "dateLastModified>='#{v.to_time.utc.xmlschema}'"
230
+ when "role"
231
+ filter << "role='#{v}'"
130
232
  else
131
233
  default_params[k] = v
132
234
  end
@@ -137,31 +239,149 @@ module Bright
137
239
  default_params.merge(params).reject{|k,v| v.respond_to?(:empty?) ? v.empty? : v.nil?}
138
240
  end
139
241
 
140
- def convert_to_student_data(student_params)
141
- return {} if student_params.nil?
142
- demographics_params = self.request(:get, "demographics/#{student_params["sourcedId"]}")["demographics"]
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
143
261
 
144
- student_data_hsh = {
145
- :api_id => student_params["sourcedId"],
146
- :first_name => student_params["givenName"],
147
- :middle_name => student_params["middleName"],
148
- :last_name => student_params["familyName"],
149
- :sis_student_id => student_params["identifier"],
150
- :last_modified => student_params["dateLastModified"]
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"]
151
269
  }
152
- unless demographics_params["birthdate"].nil?
153
- student_data_hsh[:birth_date] = Date.parse(demographics_params["birthdate"]).to_s
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"]
154
284
  end
155
- unless demographics_params["sex"].to_s[0].nil?
156
- student_data_hsh[:gender] = demographics_params["sex"].to_s[0].upcase
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
157
360
  end
158
361
  DEMOGRAPHICS_CONVERSION.each do |demographics_key, demographics_value|
159
- if demographics_params[demographics_key] == "true"
160
- student_data_hsh[:race] ||= []
161
- student_data_hsh[:race] << 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
162
382
  end
163
383
  end
164
- return student_data_hsh
384
+ return self.school_years_cache[date]
165
385
  end
166
386
 
167
387
  end