ps_utilities 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 6b53435e5d2f6da0cd21f54f5780806986b87e7ff36b963702e4cd1009862e7d
4
+ data.tar.gz: 0f3133a755d4f2f67e8bde12bbb3769969afe60015f609b672166582b06d2898
5
+ SHA512:
6
+ metadata.gz: 0b39f2d31153c8fc135d6fc39a415c55f8a347f4c97f0de18b91bec030f99d182e9baa655de000a29996d01997b2e44023f6d55d4f0b02e6187f254fd3f555b3
7
+ data.tar.gz: 87407650b7062b75f4ae4158bd48103a2acc302472923ad4d0456fbb900dc4a07191588df67621f028bcdf6e8d18aadf908dcfe8c666467335ecb872f68a6ea1
@@ -0,0 +1,166 @@
1
+ module PsUtilities
2
+
3
+ module ApiEndpoints
4
+
5
+ API_PATHS = {
6
+ ws: '/ws/v1',
7
+ ptg: '/powerschool-ptg-api/v2/',
8
+ xte: '/ws/xte'
9
+ }
10
+
11
+ def initialize(api_credentials, options = {})
12
+ self.client = Class.new(Powerschool::Client) do |klass|
13
+ uri = api_credentials['base_uri'] || Powerschool::Client::BASE_URI
14
+ klass.base_uri(uri)
15
+
16
+ # options like `verify: false` (to disable ssl verification)
17
+ options.each do |k, v|
18
+ default_options.update({k => v})
19
+ end
20
+ end.new(api_credentials)
21
+ end
22
+
23
+ class << self
24
+ [:get, :post, :put, :delete].each do |command|
25
+ define_method(command.to_s) do |method, api, path = nil|
26
+ if path.nil?
27
+ path, api = api, nil
28
+ end
29
+ define_method(method) do |options = {}|
30
+ return self.client.class.send(command, prepare_path(path.dup, api, options), self.client.options.merge(options))
31
+ end
32
+ end
33
+ end
34
+ end
35
+
36
+ def prepare_path(path, api, options)
37
+ options = options.dup
38
+ options.each_pair do |key, value|
39
+ regexp_path_option = /(:#{key}$|:#{key}([:&\/-_]))/
40
+ if path.match(regexp_path_option)
41
+ if value.blank?
42
+ raise "Blank value for parameter '%s' in '%s'" % [key, path]
43
+ end
44
+ path.gsub!(regexp_path_option, "#{value}\\2")
45
+ options.delete(key)
46
+ end
47
+ end
48
+ if parameter = path.match(/:(\w*)/)
49
+ raise "Missing parameter '%s' in '%s'. Parameters: %s" % [parameter[1], path, options]
50
+ end
51
+ if api
52
+ path = (API_PATHS[api] + path).gsub('//', '/')
53
+ end
54
+ path
55
+ end
56
+
57
+ # retreive max_page_size from metadata. Defaults to 100
58
+ def get_page_size(resource)
59
+ @metadata ||= self.metadata()
60
+ @metadata['%s_max_page_size' % resource.split('/').last.singularize] rescue 100
61
+ end
62
+
63
+ # Process every object for a resource.
64
+ def all(resource, options = {}, &block)
65
+ page_size = (options[:query][:pagesize] rescue nil) || get_page_size(resource)
66
+ _options = options.dup
67
+ _options[:query] ||= {}
68
+ _options[:query][:pagesize] ||= page_size
69
+
70
+ page = 1
71
+ results = []
72
+ begin
73
+ _options[:query][:page] = page
74
+ response = self.send(resource, _options)
75
+ results = response.parsed_response || {}
76
+ if !response.parsed_response
77
+ if response.headers['www-authenticate'].match(/Bearer error/)
78
+ raise response.headers['www-authenticate'].to_s
79
+ end
80
+ end
81
+
82
+ if results.is_a?(Hash)
83
+ plural = results.keys.first
84
+ results = results[plural][plural.singularize] || []
85
+ end
86
+ if results.is_a?(Hash)
87
+ # a rare(?) case has been observed where (in this case section_enrollment) did return a single
88
+ # data object as a hash rather than as a hash inside an array
89
+ results = [results]
90
+ end
91
+ results.each do |result|
92
+ block.call(result, response)
93
+ end
94
+ page += 1
95
+ end while results.any? && results.size == page_size
96
+ end
97
+
98
+ # client is set up per district so it returns only one district
99
+ # for urls with parameters
100
+ get :district, :ws, '/district'
101
+ get :schools, :ws, '/district/school'
102
+ get :teachers, :ws, '/staff'
103
+ get :student, :ws, '/student/:student_id'
104
+ get :students, :ws, '/student'
105
+ get :school_teachers, :ws, '/school/:school_id/staff'
106
+ get :school_students, :ws, '/school/:school_id/student'
107
+ get :school_sections, :ws, '/school/:school_id/section'
108
+ get :school_courses, :ws, '/school/:school_id/course'
109
+ get :school_terms, :ws, '/school/:school_id/term'
110
+ get :section_enrollment, :ws, '/section/:section_id/section_enrollment'
111
+
112
+ # PowerTeacher Gradebook (pre Powerschool 10)
113
+ get :assignment, :ptg, 'assignment/:id'
114
+ post :post_section_assignment, :ptg, '/section/:section_id/assignment'
115
+ put :put_assignment_scores, :ptg, '/assignment/:assignment_id/score'
116
+ put :put_assignment_score, :ptg, '/assignment/:assignment_id/student/:student_id/score'
117
+
118
+ # PowerTeacher Pro
119
+ post :xte_post_section_assignment, :xte, '/section/assignment?users_dcid=:teacher_id'
120
+ put :xte_put_assignment_scores, :xte, '/score'
121
+ get :xte_section_assignments, :xte, '/section/assignment?users_dcid=:teacher_id&section_ids=:section_id'
122
+ get :xte_section_assignment, :xte, '/section/assignment/:assignment_id?users_dcid=:teacher_id'
123
+ get :xte_teacher_category, :xte, '/teacher_category'
124
+
125
+ get :metadata, :ws, '/metadata'
126
+ get :areas, '/ws/schema/area'
127
+ get :tables, '/ws/schema/table'
128
+ get :table_records, '/ws/schema/table/:table?projection=:projection'
129
+ get :table_metadata, '/ws/schema/table/:table/metadata'
130
+ get :area_table, '/ws/schema/area/:area/table'
131
+
132
+ get :queries, '/ws/schema/query/api'
133
+
134
+ def start_year
135
+ offset = Date.today.month <= 6 ? -1 : 0
136
+ year = self.client.api_credentials['start_year'] || (Date.today.year + offset)
137
+ end
138
+
139
+ # Special method to filter terms and find the current ones
140
+ def current_terms(options, today = nil)
141
+ terms = []
142
+ today ||= Date.today.to_s(:db)
143
+ self.all(:school_terms, options) do |term, response|
144
+ if term['end_date'] >= today
145
+ terms << term
146
+ end
147
+ end
148
+ if terms.empty?
149
+ options[:query] = {q: 'start_year==%s' % start_year}
150
+ self.all(:school_terms, options) do |term, response|
151
+ if term['end_date'] >= today
152
+ terms << term
153
+ end
154
+ end
155
+ end
156
+ # now filter again for the start date and if there isn't one matching we have to return the most recent one
157
+ in_two_weeks = (Date.parse(today) + 2.weeks).to_s(:db)
158
+ active_terms = terms.select{|term| term['start_date'] <= in_two_weeks }
159
+ if active_terms.any?
160
+ return active_terms
161
+ end
162
+ return terms
163
+ end
164
+
165
+ end
166
+ end
@@ -0,0 +1,175 @@
1
+ require 'openssl'
2
+ require 'base64'
3
+ require 'json'
4
+ require 'httparty'
5
+ require 'ps_utilities/pre_built_get'
6
+ require 'ps_utilities/pre_built_put'
7
+ require 'ps_utilities/pre_built_post'
8
+
9
+ # http://blog.honeybadger.io/ruby-custom-exceptions/
10
+ class AuthError < RuntimeError
11
+ attr_reader :url, :credentials
12
+ def initialize(msg="", url="", credentials={})
13
+ @url = url
14
+ @credentials = credentials
15
+ super(msg)
16
+ end
17
+ end
18
+
19
+ module PsUtilities
20
+
21
+ # The PsUtilities, makes it east to work with the Powerschool API
22
+ # @since 0.1.0
23
+ #
24
+ # @note You should use environment variables to initialize your server.
25
+ class Connection
26
+
27
+ attr_reader :credentials, :headers
28
+
29
+ include PsUtilities::PreBuiltGet
30
+ include PsUtilities::PreBuiltPut
31
+ include PsUtilities::PreBuiltPost
32
+
33
+ def initialize(attributes: {}, headers: {})
34
+ @credentials = attr_defaults.merge(attributes)
35
+ @headers = header_defaults.merge(headers)
36
+
37
+ raise ArgumentError, "missing client_secret" if credentials[:client_secret].nil? or
38
+ credentials[:client_secret].empty?
39
+ raise ArgumentError, "missing client_id" if credentials[:client_id].nil? or
40
+ credentials[:client_id].empty?
41
+ raise ArgumentError, "missing base_uri" if credentials[:base_uri].nil? or
42
+ credentials[:base_uri].empty?
43
+ end
44
+
45
+ # with no command it just authenticates
46
+ def run(command: nil, params: {}, url: nil, options: {})
47
+ authenticate unless token_valid?
48
+ case command
49
+ when nil, :authenticate
50
+ # authenticate unless token_valid?
51
+ when :get, :put, :post
52
+ send(command, url, options) unless url.empty?
53
+ else
54
+ send(command, params)
55
+ end
56
+ end
57
+
58
+ private
59
+
60
+ # options = {query: {}}
61
+ def get(url, options={})
62
+ max_retries = 3
63
+ times_retried = 0
64
+ options = options.merge(headers)
65
+ ps_url = credentials[:base_uri] + url
66
+ begin
67
+ HTTParty.get(ps_url, options)
68
+ # self.class.get(url, query: options[:query], headers: options[:headers])
69
+ rescue Net::ReadTimeout, Net::OpenTimeout
70
+ if times_retried < max_retries
71
+ times_retried += 1
72
+ retry
73
+ else
74
+ { error: "no response (timeout) from URL: #{url}" }
75
+ end
76
+ end
77
+ end
78
+
79
+ # options = {body: {}}
80
+ def put(url, options={})
81
+ max_retries = 3
82
+ times_retried = 0
83
+ options = options.merge(headers)
84
+ ps_url = credentials[:base_uri] + url
85
+ begin
86
+ HTTParty.put(ps_url, options )
87
+ # self.class.get(url, body: options[:body], headers: options[:headers])
88
+ rescue Net::ReadTimeout, Net::OpenTimeout
89
+ if times_retried < max_retries
90
+ times_retried += 1
91
+ retry
92
+ else
93
+ { error: "no response (timeout) from URL: #{url}" }
94
+ end
95
+ end
96
+ end
97
+
98
+ # options = {body: {}}
99
+ def post(url, options={})
100
+ max_retries = 3
101
+ times_retried = 0
102
+ options = options.merge(headers)
103
+ ps_url = credentials[:base_uri] + url
104
+ begin
105
+ HTTParty.post(ps_url, options )
106
+ # self.class.get(url, body: options[:body], headers: options[:headers])
107
+ rescue Net::ReadTimeout, Net::OpenTimeout
108
+ if times_retried < max_retries
109
+ times_retried += 1
110
+ retry
111
+ else
112
+ { error: "no response (timeout) from URL: #{url}" }
113
+ end
114
+ end
115
+ end
116
+
117
+ # In PowerSchool go to System>System Settings>Plugin Management Configuration>your plugin>Data Provider Configuration to manually check plugin expiration date
118
+ def authenticate
119
+ @headers[:headers] ||= {}
120
+ ps_url = credentials[:base_uri] + credentials[:auth_endpoint]
121
+ response = HTTParty.post(ps_url, {headers: auth_headers,
122
+ body: 'grant_type=client_credentials'})
123
+
124
+ @credentials[:token_expires] = Time.now + response.parsed_response['expires_in'].to_i
125
+ @credentials[:access_token] = response.parsed_response['access_token'].to_s
126
+ @headers[:headers].merge!('Authorization' => 'Bearer ' + credentials[:access_token])
127
+
128
+ # throw error if no token returned -- nothing else will work
129
+ raise AuthError.new("No Auth Token Returned",
130
+ ps_url, credentials
131
+ ) if credentials[:access_token].empty?
132
+ end
133
+
134
+ def token_valid?(tokens = credentials)
135
+ return false if tokens[:access_token].nil?
136
+ return false if tokens[:access_token].empty?
137
+ return false if tokens[:token_expires].nil?
138
+ return false if tokens[:token_expires] <= Time.now
139
+ return true
140
+ end
141
+
142
+ def auth_headers
143
+ { 'ContentType' => 'application/x-www-form-urlencoded;charset=UTF-8',
144
+ 'Accept' => 'application/json',
145
+ 'Authorization' => 'Basic ' + encode_credentials
146
+ }
147
+ end
148
+
149
+ def encode_credentials
150
+ ps_auth_text = [ credentials[:client_id],
151
+ credentials[:client_secret]
152
+ ].join(':')
153
+ Base64.encode64(ps_auth_text).gsub(/\n/, '')
154
+ end
155
+
156
+ def attr_defaults
157
+ { base_uri: ENV['PS_URL'],
158
+ auth_endpoint: ENV['PS_AUTH_ENDPOINT'] || '/oauth/access_token',
159
+ client_id: ENV['PS_CLIENT_ID'],
160
+ client_secret: ENV['PS_CLIENT_SECRET'],
161
+ # not recommended here - it changes (ok as a parameter though)
162
+ # access_token: ENV['PS_ACCESS_TOKEN'] || nil,
163
+ }
164
+ end
165
+
166
+ def header_defaults
167
+ { headers:
168
+ { 'User-Agent' => "PsUtilitiesGem - v#{PsUtilities::Version::VERSION}",
169
+ 'Accept' => 'application/json',
170
+ 'Content-Type' => 'application/json'}
171
+ }
172
+ end
173
+
174
+ end
175
+ end
@@ -0,0 +1,36 @@
1
+ module PsUtilities
2
+
3
+ module PreBuiltGet
4
+
5
+ def get_active_students_count(params={})
6
+ # url = "/ws/v1/district/student/count?q=school_enrollment.enroll_status_code==0"
7
+ url = "/ws/v1/district/student/count"
8
+ options = { query: {"q" => "school_enrollment.enroll_status_code==0"} }
9
+ get(url, options)
10
+ end
11
+
12
+ def get_active_students_info(params={})
13
+ # url = "/ws/v1/district/student?q=school_enrollment.enroll_status==a&pagesize=500"
14
+ url = "/ws/v1/district/student"
15
+ options = { query:
16
+ {"q" => "school_enrollment.enroll_status_code==0",
17
+ "pagesize" => "500"}
18
+ }
19
+ get(url, options)
20
+ end
21
+
22
+ # las-test.powerschool.com/ws/v1/district/student?expansions=school_enrollment&q=student_username==xxxxx237
23
+ # params = {username: "xxxxxxx"}
24
+ def get_one_student_record(params)
25
+ # url = "/ws/v1/district/student?expansions=school_enrollment,contact&q=student_username==xxxxxx237"
26
+ url = "/ws/v1/district/student"
27
+ options = { query:
28
+ {"q" => "student_username==#{params[:username]}",
29
+ "expansions" => "school_enrollment,contact,contact_info"}
30
+ }
31
+ get(url, options)
32
+ end
33
+
34
+ end
35
+
36
+ end
@@ -0,0 +1,7 @@
1
+ module PsUtilities
2
+
3
+ module PreBuiltPost
4
+
5
+ end
6
+
7
+ end
@@ -0,0 +1,7 @@
1
+ module PsUtilities
2
+
3
+ module PreBuiltPut
4
+
5
+ end
6
+
7
+ end
@@ -0,0 +1,5 @@
1
+ module PsUtilities
2
+ module Version
3
+ VERSION = "0.1.0"
4
+ end
5
+ end
@@ -0,0 +1,6 @@
1
+ require "ps_utilities/version"
2
+ require "ps_utilities/connection"
3
+
4
+ module PsUtilities
5
+ # Your code goes here...
6
+ end
metadata ADDED
@@ -0,0 +1,110 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: ps_utilities
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Lee Weisbecker
8
+ - Bill Tihen
9
+ autorequire:
10
+ bindir: exe
11
+ cert_chain: []
12
+ date: 2018-06-21 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: httparty
16
+ requirement: !ruby/object:Gem::Requirement
17
+ requirements:
18
+ - - "~>"
19
+ - !ruby/object:Gem::Version
20
+ version: '0.16'
21
+ type: :runtime
22
+ prerelease: false
23
+ version_requirements: !ruby/object:Gem::Requirement
24
+ requirements:
25
+ - - "~>"
26
+ - !ruby/object:Gem::Version
27
+ version: '0.16'
28
+ - !ruby/object:Gem::Dependency
29
+ name: bundler
30
+ requirement: !ruby/object:Gem::Requirement
31
+ requirements:
32
+ - - "~>"
33
+ - !ruby/object:Gem::Version
34
+ version: '1.16'
35
+ type: :development
36
+ prerelease: false
37
+ version_requirements: !ruby/object:Gem::Requirement
38
+ requirements:
39
+ - - "~>"
40
+ - !ruby/object:Gem::Version
41
+ version: '1.16'
42
+ - !ruby/object:Gem::Dependency
43
+ name: rake
44
+ requirement: !ruby/object:Gem::Requirement
45
+ requirements:
46
+ - - "~>"
47
+ - !ruby/object:Gem::Version
48
+ version: '10.5'
49
+ type: :development
50
+ prerelease: false
51
+ version_requirements: !ruby/object:Gem::Requirement
52
+ requirements:
53
+ - - "~>"
54
+ - !ruby/object:Gem::Version
55
+ version: '10.5'
56
+ - !ruby/object:Gem::Dependency
57
+ name: rspec
58
+ requirement: !ruby/object:Gem::Requirement
59
+ requirements:
60
+ - - "~>"
61
+ - !ruby/object:Gem::Version
62
+ version: '3.7'
63
+ type: :development
64
+ prerelease: false
65
+ version_requirements: !ruby/object:Gem::Requirement
66
+ requirements:
67
+ - - "~>"
68
+ - !ruby/object:Gem::Version
69
+ version: '3.7'
70
+ description: 'Uses oauth2 to connection to the Powerschool API. Heavily refactored
71
+ code (not dependent on Rails) starting with: https://github.com/TomK32/powerschool'
72
+ email:
73
+ - leeweisbecker@gmail.com
74
+ - btihen@gmail.com
75
+ executables: []
76
+ extensions: []
77
+ extra_rdoc_files: []
78
+ files:
79
+ - lib/ps_utilities.rb
80
+ - lib/ps_utilities/api_endpoints.rb
81
+ - lib/ps_utilities/connection.rb
82
+ - lib/ps_utilities/pre_built_get.rb
83
+ - lib/ps_utilities/pre_built_post.rb
84
+ - lib/ps_utilities/pre_built_put.rb
85
+ - lib/ps_utilities/version.rb
86
+ homepage: https://github.com/LAS-IT/ps_utilities
87
+ licenses:
88
+ - MIT
89
+ metadata: {}
90
+ post_install_message:
91
+ rdoc_options: []
92
+ require_paths:
93
+ - lib
94
+ required_ruby_version: !ruby/object:Gem::Requirement
95
+ requirements:
96
+ - - ">="
97
+ - !ruby/object:Gem::Version
98
+ version: '0'
99
+ required_rubygems_version: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ requirements: []
105
+ rubyforge_project:
106
+ rubygems_version: 2.7.6
107
+ signing_key:
108
+ specification_version: 4
109
+ summary: Simple ruby wrapper for Powerschool API interaction.
110
+ test_files: []