bright 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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