bright 0.1.0
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.
- checksums.yaml +7 -0
- data/.gitignore +14 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +31 -0
- data/Rakefile +2 -0
- data/bright.gemspec +26 -0
- data/lib/bright.rb +20 -0
- data/lib/bright/address.rb +17 -0
- data/lib/bright/connection.rb +119 -0
- data/lib/bright/enrollment.rb +7 -0
- data/lib/bright/errors.rb +24 -0
- data/lib/bright/model.rb +47 -0
- data/lib/bright/response_collection.rb +55 -0
- data/lib/bright/school.rb +17 -0
- data/lib/bright/sis_apis/aeries.rb +145 -0
- data/lib/bright/sis_apis/base.rb +26 -0
- data/lib/bright/sis_apis/infinite_campus.rb +31 -0
- data/lib/bright/sis_apis/power_school.rb +386 -0
- data/lib/bright/sis_apis/synergy.rb +31 -0
- data/lib/bright/sis_apis/tsis.rb +248 -0
- data/lib/bright/student.rb +53 -0
- data/lib/bright/version.rb +3 -0
- metadata +126 -0
@@ -0,0 +1,26 @@
|
|
1
|
+
module Bright
|
2
|
+
module SisApi
|
3
|
+
class Base
|
4
|
+
|
5
|
+
def filter_students_by_params(students, params)
|
6
|
+
total = params[:limit]
|
7
|
+
count = 0
|
8
|
+
found = []
|
9
|
+
|
10
|
+
keys = (Student.attribute_names & params.keys.collect(&:to_sym))
|
11
|
+
puts "filtering on #{keys.join(",")}"
|
12
|
+
students.each do |student|
|
13
|
+
break if total and count >= total
|
14
|
+
|
15
|
+
should = (keys).all? do |m|
|
16
|
+
student.send(m) =~ Regexp.new(Regexp.escape(params[m]), Regexp::IGNORECASE)
|
17
|
+
end
|
18
|
+
count += 1 if total and should
|
19
|
+
found << student if should
|
20
|
+
end
|
21
|
+
found
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module Bright
|
2
|
+
module SisApi
|
3
|
+
class InfiniteCampus < Base
|
4
|
+
|
5
|
+
def get_student_by_api_id(api_id)
|
6
|
+
raise NotImplementedError
|
7
|
+
end
|
8
|
+
|
9
|
+
def get_student(params)
|
10
|
+
raise NotImplementedError
|
11
|
+
end
|
12
|
+
|
13
|
+
def get_students(params)
|
14
|
+
raise NotImplementedError
|
15
|
+
end
|
16
|
+
|
17
|
+
def create_student(student, additional_params = {})
|
18
|
+
raise NotImplementedError
|
19
|
+
end
|
20
|
+
|
21
|
+
def update_student(student, additional_params = {})
|
22
|
+
raise NotImplementedError
|
23
|
+
end
|
24
|
+
|
25
|
+
def get_schools(params)
|
26
|
+
raise NotImplementedError
|
27
|
+
end
|
28
|
+
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,386 @@
|
|
1
|
+
module Bright
|
2
|
+
module SisApi
|
3
|
+
class PowerSchool < Base
|
4
|
+
DATE_FORMAT = '%Y-%m-%d'
|
5
|
+
INVALID_SEARCH_CHAR_RE = /[\,\;]/
|
6
|
+
|
7
|
+
@@description = "Connects to the PowerSchool API for accessing student information"
|
8
|
+
@@doc_url = ""
|
9
|
+
@@api_version = "1.6.0"
|
10
|
+
|
11
|
+
attr_accessor :connection_options, :expansion_options
|
12
|
+
|
13
|
+
def initialize(options = {})
|
14
|
+
self.connection_options = options[:connection] || {}
|
15
|
+
self.expansion_options = options[:expansion] || {}
|
16
|
+
# {
|
17
|
+
# :client_id => "",
|
18
|
+
# :client_secret => "",
|
19
|
+
# :uri => ""
|
20
|
+
# :access_token => "", #optional
|
21
|
+
# }
|
22
|
+
end
|
23
|
+
|
24
|
+
def get_student_by_api_id(api_id, params = {})
|
25
|
+
params = self.apply_expansions(params)
|
26
|
+
st_hsh = self.request(:get, "ws/v1/student/#{api_id}", params)
|
27
|
+
Student.new(convert_to_student_data(st_hsh["student"])) if st_hsh and st_hsh["student"]
|
28
|
+
end
|
29
|
+
|
30
|
+
def get_student(params = {}, options = {})
|
31
|
+
self.get_students(params, options.merge(:per_page => 1, :wrap_in_collection => false)).first
|
32
|
+
end
|
33
|
+
|
34
|
+
def get_students(params = {}, options = {})
|
35
|
+
params = self.apply_expansions(params)
|
36
|
+
params = self.apply_options(params, options)
|
37
|
+
|
38
|
+
if options[:wrap_in_collection] != false
|
39
|
+
students_count_response_hash = self.request(:get, 'ws/v1/district/student/count', self.map_student_search_params(params))
|
40
|
+
# {"resource"=>{"count"=>293}}
|
41
|
+
total_results = students_count_response_hash["resource"]["count"].to_i if students_count_response_hash["resource"]
|
42
|
+
end
|
43
|
+
|
44
|
+
students_response_hash = self.request(:get, 'ws/v1/district/student', self.map_student_search_params(params))
|
45
|
+
if students_response_hash and students_response_hash["students"] && students_response_hash["students"]["student"]
|
46
|
+
students_hash = [students_response_hash["students"]["student"]].flatten
|
47
|
+
|
48
|
+
students = students_hash.compact.collect {|st_hsh|
|
49
|
+
Student.new(convert_to_student_data(st_hsh))
|
50
|
+
}
|
51
|
+
|
52
|
+
if options[:wrap_in_collection] != false
|
53
|
+
api = self
|
54
|
+
load_more_call = proc { |page|
|
55
|
+
# pages start at one, so add a page here
|
56
|
+
api.get_students(params, {:wrap_in_collection => false, :page => (page + 1)})
|
57
|
+
}
|
58
|
+
|
59
|
+
ResponseCollection.new({
|
60
|
+
:seed_page => students,
|
61
|
+
:total => total_results,
|
62
|
+
:per_page => params[:pagesize],
|
63
|
+
:load_more_call => load_more_call
|
64
|
+
})
|
65
|
+
else
|
66
|
+
students
|
67
|
+
end
|
68
|
+
else
|
69
|
+
[]
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def create_student(student, additional_params = {})
|
74
|
+
response = self.request(:post, 'ws/v1/student', self.convert_from_student_data(student, "INSERT", additional_params))
|
75
|
+
if response["results"] and response["results"]["insert_count"] == 1
|
76
|
+
student.api_id = response["results"]["result"]["success_message"]["id"]
|
77
|
+
|
78
|
+
# update our local student object with any data the server might have updated
|
79
|
+
nstudent = self.get_student_by_api_id(student.api_id)
|
80
|
+
student.assign_attributes(Hash[Bright::Student.attribute_names.collect{|n| [n, nstudent.send(n)]}].reject{|k,v| v.nil?})
|
81
|
+
|
82
|
+
# enrollment is no longer needed as creation is over
|
83
|
+
student.enrollment = nil
|
84
|
+
nstudent = nil
|
85
|
+
else
|
86
|
+
puts response.inspect
|
87
|
+
end
|
88
|
+
student
|
89
|
+
end
|
90
|
+
|
91
|
+
def update_student(student, additional_params = {})
|
92
|
+
response = self.request(:post, 'ws/v1/student', self.convert_from_student_data(student, "UPDATE", additional_params))
|
93
|
+
if response["results"] and response["results"]["update_count"] == 1
|
94
|
+
student.api_id = response["results"]["result"]["success_message"]["id"]
|
95
|
+
self.get_student_by_api_id(student.api_id)
|
96
|
+
else
|
97
|
+
puts response.inspect
|
98
|
+
student
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
def subscribe_student(student)
|
103
|
+
raise NotImplementedError
|
104
|
+
end
|
105
|
+
|
106
|
+
def get_schools(params = {}, options = {})
|
107
|
+
params = self.apply_options(params, options)
|
108
|
+
|
109
|
+
if options[:wrap_in_collection] != false
|
110
|
+
schools_count_response_hash = self.request(:get, 'ws/v1/district/school/count', params)
|
111
|
+
# {"resource"=>{"count"=>293}}
|
112
|
+
total_results = schools_count_response_hash["resource"]["count"].to_i if schools_count_response_hash["resource"]
|
113
|
+
end
|
114
|
+
|
115
|
+
schools_response_hash = self.request(:get, 'ws/v1/district/school', params)
|
116
|
+
puts schools_response_hash.inspect
|
117
|
+
schools_hsh = [schools_response_hash["schools"]["school"]].flatten
|
118
|
+
|
119
|
+
schools = schools_hsh.compact.collect {|st_hsh|
|
120
|
+
School.new(convert_to_school_data(st_hsh))
|
121
|
+
}
|
122
|
+
|
123
|
+
if options[:wrap_in_collection] != false
|
124
|
+
api = self
|
125
|
+
load_more_call = proc { |page|
|
126
|
+
# pages start at one, so add a page here
|
127
|
+
api.get_schools(params, {:wrap_in_collection => false, :page => (page + 1)})
|
128
|
+
}
|
129
|
+
|
130
|
+
ResponseCollection.new({
|
131
|
+
:seed_page => schools,
|
132
|
+
:total => total_results,
|
133
|
+
:per_page => params[:pagesize],
|
134
|
+
:load_more_call => load_more_call
|
135
|
+
})
|
136
|
+
else
|
137
|
+
schools
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
def retrive_access_token
|
142
|
+
connection = Bright::Connection.new("#{self.connection_options[:uri]}/oauth/access_token/")
|
143
|
+
response = connection.request(:post, "grant_type=client_credentials", self.headers_for_access_token)
|
144
|
+
if !response.error?
|
145
|
+
response_hash = JSON.parse(response.body)
|
146
|
+
end
|
147
|
+
if response_hash["access_token"]
|
148
|
+
self.connection_options[:access_token] = response_hash["access_token"]
|
149
|
+
end
|
150
|
+
response_hash
|
151
|
+
end
|
152
|
+
|
153
|
+
def request(method, path, params = {})
|
154
|
+
uri = "#{self.connection_options[:uri]}/#{path}"
|
155
|
+
body = nil
|
156
|
+
if method == :get
|
157
|
+
query = URI.encode_www_form(params)
|
158
|
+
uri += "?#{query}"
|
159
|
+
else
|
160
|
+
body = JSON.dump(params)
|
161
|
+
end
|
162
|
+
|
163
|
+
headers = self.headers_for_auth
|
164
|
+
|
165
|
+
connection = Bright::Connection.new(uri)
|
166
|
+
response = connection.request(method, body, headers)
|
167
|
+
|
168
|
+
if !response.error?
|
169
|
+
response_hash = JSON.parse(response.body)
|
170
|
+
else
|
171
|
+
puts "#{response.inspect}"
|
172
|
+
puts "#{response.body}"
|
173
|
+
end
|
174
|
+
response_hash
|
175
|
+
end
|
176
|
+
|
177
|
+
protected
|
178
|
+
|
179
|
+
def map_student_search_params(params)
|
180
|
+
params = params.dup
|
181
|
+
default_params = {}
|
182
|
+
|
183
|
+
q = ""
|
184
|
+
%w(first_name middle_name last_name).each do |f|
|
185
|
+
if fn = params.delete(f.to_sym)
|
186
|
+
fn = fn.gsub(INVALID_SEARCH_CHAR_RE, " ").strip
|
187
|
+
q += %(name.#{f}==#{fn};)
|
188
|
+
end
|
189
|
+
end
|
190
|
+
if lid = params.delete(:sis_student_id)
|
191
|
+
lid = lid.gsub(INVALID_SEARCH_CHAR_RE, " ").strip
|
192
|
+
q += %(local_id==#{lid};)
|
193
|
+
end
|
194
|
+
if sid = params.delete(:state_student_id)
|
195
|
+
sid = sid.gsub(INVALID_SEARCH_CHAR_RE, " ").strip
|
196
|
+
q += %(state_province_id==#{sid};)
|
197
|
+
end
|
198
|
+
params[:q] = q
|
199
|
+
|
200
|
+
default_params.merge(params).reject{|k,v| v.respond_to?(:empty?) ? v.empty? : v.nil?}
|
201
|
+
end
|
202
|
+
|
203
|
+
def convert_to_student_data(attrs)
|
204
|
+
cattrs = {}
|
205
|
+
|
206
|
+
if attrs["name"]
|
207
|
+
cattrs[:first_name] = attrs["name"]["first_name"]
|
208
|
+
cattrs[:middle_name] = attrs["name"]["middle_name"]
|
209
|
+
cattrs[:last_name] = attrs["name"]["last_name"]
|
210
|
+
end
|
211
|
+
|
212
|
+
cattrs[:api_id] = attrs["id"].to_s
|
213
|
+
cattrs[:sis_student_id] = attrs["local_id"].to_s
|
214
|
+
cattrs[:state_student_id] = attrs["state_province_id"].to_s
|
215
|
+
|
216
|
+
if attrs["demographics"]
|
217
|
+
if attrs["demographics"]["birth_date"]
|
218
|
+
begin
|
219
|
+
cattrs[:birth_date] = Date.strptime(attrs["demographics"]["birth_date"], DATE_FORMAT)
|
220
|
+
rescue => e
|
221
|
+
puts "#{e.inspect} #{bd}"
|
222
|
+
end
|
223
|
+
end
|
224
|
+
|
225
|
+
cattrs[:gender] = attrs["demographics"]["gender"]
|
226
|
+
|
227
|
+
pg = attrs["demographics"]["projected_graduation_year"].to_i
|
228
|
+
cattrs[:projected_graduation_year] = pg if pg > 0
|
229
|
+
end
|
230
|
+
|
231
|
+
begin
|
232
|
+
cattrs[:addresses] = attrs["addresses"].to_a.collect{|a| self.convert_to_address_data(a)} if attrs["addresses"]
|
233
|
+
rescue
|
234
|
+
end
|
235
|
+
cattrs.reject{|k,v| v.respond_to?(:empty?) ? v.empty? : v.nil?}
|
236
|
+
end
|
237
|
+
|
238
|
+
def convert_from_student_data(student, action = nil, additional_params = {})
|
239
|
+
return {} if student.nil?
|
240
|
+
|
241
|
+
student_data = {
|
242
|
+
:client_uid => student.client_id,
|
243
|
+
:action => action,
|
244
|
+
:id => student.api_id,
|
245
|
+
:local_id => student.sis_student_id,
|
246
|
+
:state_province_id => student.state_student_id,
|
247
|
+
:name => {
|
248
|
+
:first_name => student.first_name,
|
249
|
+
:middle_name => student.middle_name,
|
250
|
+
:last_name => student.last_name
|
251
|
+
}.reject{|k,v| v.respond_to?(:empty?) ? v.empty? : v.nil?},
|
252
|
+
:demographics => {
|
253
|
+
:gender => student.gender.to_s[0].to_s.upcase,
|
254
|
+
:birth_date => (student.birth_date ? student.birth_date.strftime(DATE_FORMAT) : nil),
|
255
|
+
:projected_graduation_year => student.projected_graduation_year
|
256
|
+
}.reject{|k,v| v.respond_to?(:empty?) ? v.empty? : v.nil?}
|
257
|
+
}.merge(additional_params).reject{|k,v| v.respond_to?(:empty?) ? v.empty? : v.nil?}
|
258
|
+
|
259
|
+
# apply enrollment info
|
260
|
+
if student.enrollment
|
261
|
+
student_data.merge!(self.convert_from_enrollment_data(student.enrollment))
|
262
|
+
end
|
263
|
+
|
264
|
+
# apply addresses
|
265
|
+
address_data = {}
|
266
|
+
if ph = student.addresses.detect{|a| a.type == "physical"}
|
267
|
+
address_data.merge!(self.convert_from_address_data(ph))
|
268
|
+
end
|
269
|
+
if mail = student.addresses.detect{|a| a.type == "mailing"}
|
270
|
+
address_data.merge!(self.convert_from_address_data(mail))
|
271
|
+
end
|
272
|
+
if ph.nil? and mail.nil? and any = student.addresses.first
|
273
|
+
cany = any.clone
|
274
|
+
cany.type = "physical"
|
275
|
+
address_data.merge!(self.convert_from_address_data(cany))
|
276
|
+
end
|
277
|
+
if address_data.size > 0
|
278
|
+
student_data.merge!({:addresses => address_data})
|
279
|
+
end
|
280
|
+
|
281
|
+
{:students => {:student => student_data}}
|
282
|
+
end
|
283
|
+
|
284
|
+
def convert_from_enrollment_data(enrollment)
|
285
|
+
return {} if enrollment.nil?
|
286
|
+
{:school_enrollment => {
|
287
|
+
:enroll_status => "A",
|
288
|
+
:entry_date => (enrollment.entry_date || Date.today).strftime(DATE_FORMAT),
|
289
|
+
:entry_comment => enrollment.entry_comment,
|
290
|
+
:exit_date => (enrollment.exit_date || enrollment.entry_date || Date.today).strftime(DATE_FORMAT),
|
291
|
+
:exit_comment => enrollment.exit_comment,
|
292
|
+
:grade_level => enrollment.grade,
|
293
|
+
:school_number => enrollment.school ? enrollment.school.number : nil
|
294
|
+
}.reject{|k,v| v.respond_to?(:empty?) ? v.empty? : v.nil?}
|
295
|
+
}
|
296
|
+
end
|
297
|
+
|
298
|
+
def convert_to_school_data(attrs)
|
299
|
+
cattrs = {}
|
300
|
+
|
301
|
+
cattrs[:api_id] = attrs["id"]
|
302
|
+
cattrs[:name] = attrs["name"]
|
303
|
+
cattrs[:number] = attrs["school_number"]
|
304
|
+
|
305
|
+
cattrs.reject{|k,v| v.respond_to?(:empty?) ? v.empty? : v.nil?}
|
306
|
+
end
|
307
|
+
|
308
|
+
def convert_from_address_data(address)
|
309
|
+
{
|
310
|
+
(address.type || "physcial") => {
|
311
|
+
:street => "#{address.street} #{address.apt}", # powerschool doesn't appear to support passing the apt in the api
|
312
|
+
:city => address.city,
|
313
|
+
:state_province => address.state,
|
314
|
+
:postal_code => address.postal_code,
|
315
|
+
:grid_location => address.geographical_coordinates.to_s.gsub(",", ", ") # make sure there is a comma + space
|
316
|
+
}.reject{|k,v| v.respond_to?(:empty?) ? v.empty? : v.nil?}
|
317
|
+
}
|
318
|
+
end
|
319
|
+
|
320
|
+
def convert_to_address_data(attrs)
|
321
|
+
cattrs = {}
|
322
|
+
|
323
|
+
if attrs.is_a?(Array)
|
324
|
+
if attrs.first.is_a?(String)
|
325
|
+
cattrs[:type] = attrs.first
|
326
|
+
attrs = attrs.last
|
327
|
+
else
|
328
|
+
attrs = attrs.first
|
329
|
+
end
|
330
|
+
else
|
331
|
+
cattrs[:type] = attrs.keys.first
|
332
|
+
attrs = attrs.values.first
|
333
|
+
end
|
334
|
+
|
335
|
+
cattrs[:street] = attrs["street"]
|
336
|
+
cattrs[:city] = attrs["city"]
|
337
|
+
cattrs[:state] = attrs["state_province"]
|
338
|
+
cattrs[:postal_code] = attrs["postal_code"]
|
339
|
+
if attrs["grid_location"] and lat_lng = attrs["grid_location"].split(/,\s?/)
|
340
|
+
cattrs[:lattitude], cattrs[:longitude] = lat_lng
|
341
|
+
end
|
342
|
+
|
343
|
+
cattrs.reject{|k,v| v.respond_to?(:empty?) ? v.empty? : v.nil?}
|
344
|
+
end
|
345
|
+
|
346
|
+
def apply_expansions(params)
|
347
|
+
if self.expansion_options.empty?
|
348
|
+
hsh = self.request(:get, 'ws/v1/district/student', {:pagesize => 1, :q => "local_id==0"})
|
349
|
+
if hsh and hsh["students"]
|
350
|
+
self.expansion_options = {
|
351
|
+
:expansions => hsh["students"]["@expansions"].to_s.split(/\,\s?/),
|
352
|
+
:extensions => hsh["students"]["@extensions"].to_s.split(/\,\s?/),
|
353
|
+
}
|
354
|
+
end
|
355
|
+
end
|
356
|
+
|
357
|
+
params.merge({
|
358
|
+
:expansions => (%w(demographics addresses ethnicity_race phones contact contact_info) & (self.expansion_options[:expansions] || [])).join(","),
|
359
|
+
:extensions => (%w(studentcorefields) & (self.expansion_options[:extensions] || [])).join(",")
|
360
|
+
}.reject{|k,v| v.empty?})
|
361
|
+
end
|
362
|
+
|
363
|
+
def apply_options(params, options)
|
364
|
+
options[:per_page] = params[:pagesize] ||= params.delete(:per_page) || options[:per_page] || 100
|
365
|
+
params[:page] ||= options[:page] || 1
|
366
|
+
params
|
367
|
+
end
|
368
|
+
|
369
|
+
def headers_for_access_token
|
370
|
+
{
|
371
|
+
"Authorization" => "Basic #{Base64.strict_encode64("#{self.connection_options[:client_id]}:#{self.connection_options[:client_secret]}")}",
|
372
|
+
"Content-Type" => "application/x-www-form-urlencoded;charset=UTF-8"
|
373
|
+
}
|
374
|
+
end
|
375
|
+
|
376
|
+
def headers_for_auth
|
377
|
+
self.retrive_access_token if self.connection_options[:access_token].nil?
|
378
|
+
{
|
379
|
+
"Authorization" => "Bearer #{self.connection_options[:access_token]}",
|
380
|
+
"Accept" => "application/json;charset=UTF-8",
|
381
|
+
"Content-Type" =>"application/json;charset=UTF-8"
|
382
|
+
}
|
383
|
+
end
|
384
|
+
end
|
385
|
+
end
|
386
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module Bright
|
2
|
+
module SisApi
|
3
|
+
class Synergy < Base
|
4
|
+
|
5
|
+
def get_student_by_api_id(api_id)
|
6
|
+
raise NotImplementedError
|
7
|
+
end
|
8
|
+
|
9
|
+
def get_student(params)
|
10
|
+
raise NotImplementedError
|
11
|
+
end
|
12
|
+
|
13
|
+
def get_students(params)
|
14
|
+
raise NotImplementedError
|
15
|
+
end
|
16
|
+
|
17
|
+
def create_student(student, additional_params = {})
|
18
|
+
raise NotImplementedError
|
19
|
+
end
|
20
|
+
|
21
|
+
def update_student(student, additional_params = {})
|
22
|
+
raise NotImplementedError
|
23
|
+
end
|
24
|
+
|
25
|
+
def get_schools(params)
|
26
|
+
raise NotImplementedError
|
27
|
+
end
|
28
|
+
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|