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.
@@ -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