uw_sws 1.0.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.
data/README.md ADDED
@@ -0,0 +1,63 @@
1
+ # UW Student Web Service
2
+ This implements almost all of the public and private [v4 UW Student Webservice
3
+ endpoints](https://wiki.cac.washington.edu/display/SWS/Student+Web+Service+Client+Home+Page). It's designed to fetch the JSON endpoints and return a Hash. This gem has the capability to cache all web requests to assit with speedy development.
4
+
5
+ ## USE
6
+
7
+ ### Installation (for use in your project)
8
+
9
+ gem install uw_sws
10
+
11
+ ### Examples
12
+ Basic example below gives you hash of term data for winter 2013
13
+
14
+ require 'uw_student_webservice'
15
+ service = UwStudentWebService.new
16
+ term = service.term(2013, "winter")
17
+
18
+ Maybe you want all the Geology courses from 1985?
19
+
20
+ require 'uw_student_webservice'
21
+ service = UwStudentWebService.new
22
+ courses = service(1985, "winter", curriculum: "GEOG")
23
+
24
+ For cases where you need to page through results you can check for the existance
25
+ of ``service.next`` and make follow up queries based on it's data.
26
+
27
+ require 'uw_student_webservice'
28
+ service = UWStudentWebService.new
29
+ courses = service.courses(1985, "autumn", curriculum: "GEOG", size: 25)
30
+ puts service.next
31
+
32
+ For a full list of examples see /test
33
+
34
+ ### Caching
35
+
36
+ If you pass ``use_cache: true`` as a parameter to ``UWStudentWebService.new`` all web requests will be cached in your local file system. However, you will need to have a cache directory in the root of whatever projects you are using this gem in.
37
+
38
+
39
+ ## Development
40
+
41
+ ### Installation
42
+
43
+ git clone git@github.com:UWFosterIT/uwsws.git
44
+ cd uwsws
45
+ bundle install
46
+
47
+ ### Setup and Tests
48
+ Make sure to delete /test/test_private_endpoints.rb if you are not authorized at those endpoints otherwise half the tests will fail. Change the ``cache`` symlink to point a valid path or create a directory for it like below.
49
+
50
+ rm cache
51
+ mkdir cache
52
+ rake
53
+
54
+ ### Contributing
55
+
56
+ 1. Fork it
57
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
58
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
59
+ 4. Push to the branch (`git push origin my-new-feature`)
60
+ 5. Create new Pull Request
61
+
62
+ ## TO DO
63
+ The end points are going through some major changes this year (2014) and testing of those changes will be available in June. A new major version will most likely be created to incorporate those changes since they are changing the way authorization works.
data/Rakefile ADDED
@@ -0,0 +1,9 @@
1
+ require "bundler/gem_tasks"
2
+ require 'rake/testtask'
3
+
4
+ Rake::TestTask.new do |t|
5
+ t.libs << 'test'
6
+ end
7
+
8
+ desc "Run tests"
9
+ task :default => :test
@@ -0,0 +1,3 @@
1
+ module UwSws
2
+ VERSION = "1.0.2"
3
+ end
data/lib/uw_sws.rb ADDED
@@ -0,0 +1,303 @@
1
+ require "restclient"
2
+ require "logger"
3
+ require "json"
4
+
5
+ class UwSws
6
+ attr_reader :last, :next
7
+
8
+ def initialize(throw_404: true, logger: Logger.new(STDOUT),
9
+ use_cache: true, cert: "", key: "", throw_HEPPS: true)
10
+ @base = "https://ws.admin.washington.edu/student/v4/public/"
11
+ @base_private = "https://ws.admin.washington.edu/student/v4/"
12
+ @last = nil
13
+ @next = ""
14
+ @use_cache = use_cache
15
+ @logger = logger
16
+ @throw_404 = throw_404
17
+ @throw_HEPPS = throw_HEPPS
18
+ @private_endpoint = false
19
+ load_config(cert, key)
20
+ end
21
+
22
+ def campus
23
+ parse("#{endpoint}campus.json")
24
+ end
25
+
26
+ def colleges(campus)
27
+ data = parse("#{endpoint}college.json?campus_short_name=#{campus}")
28
+
29
+ data["Colleges"]
30
+ end
31
+
32
+ def departments(college)
33
+ fix_param(college)
34
+ data = parse("#{endpoint}department.json?college_abbreviation=#{college}")
35
+
36
+ data["Departments"]
37
+ end
38
+
39
+ def curricula(year, quarter, department: "", count: 0)
40
+ fix_param(department)
41
+ data = parse("#{endpoint}curriculum.json?year=#{year}&quarter=#{quarter}"\
42
+ "&future_terms=#{count}&department_abbreviation=#{department}")
43
+
44
+ data["Curricula"]
45
+ end
46
+
47
+ def course(year, quarter, curriculum, number, is_private: false)
48
+ fix_param(curriculum)
49
+ data = parse("#{endpoint(is_private)}course/#{year},#{quarter},"\
50
+ "#{curriculum},#{number}.json")
51
+ data
52
+ end
53
+
54
+ def term(year, quarter)
55
+ parse("#{endpoint}term/#{year},#{quarter}.json")
56
+ end
57
+
58
+ def term_current
59
+ parse("#{endpoint}term/current.json")
60
+ end
61
+
62
+ def term_next
63
+ parse("#{endpoint}term/next.json")
64
+ end
65
+
66
+ def term_previous
67
+ parse("#{endpoint}term/previous.json")
68
+ end
69
+
70
+ def sections(year, curriculum: "", instructor: "", count: 0, quarter: "",
71
+ course_num: "", is_private: false)
72
+ fix_param(curriculum)
73
+ data = parse("#{endpoint(is_private)}section.json?year=#{year}"\
74
+ "&quarter=#{quarter}&curriculum_abbreviation=#{curriculum}"\
75
+ "&future_terms=#{count}&course_number=#{course_num}"\
76
+ "&reg_id=#{instructor}")
77
+
78
+ data["Sections"]
79
+ end
80
+
81
+ def courses(year, quarter, curriculum: "", course: "", has_sections: "",
82
+ size: 100, start: "", count: "", get_next: false)
83
+ if get_next
84
+ url = @next.sub("student/v4/public/", "")
85
+ data = parse("#{endpoint}#{url}")
86
+ else
87
+ fix_param(curriculum)
88
+ data = parse("#{endpoint}course.json?&year=#{year}&quarter=#{quarter}"\
89
+ "&curriculum_abbreviation=#{curriculum}&"\
90
+ "course_number=#{course}&page_size=#{size}"\
91
+ "&page_start=#{start}"\
92
+ "&exclude_courses_without_sections=#{has_sections}&"\
93
+ "future_terms=#{count}")
94
+ end
95
+
96
+ data["Courses"]
97
+ end
98
+
99
+ def section(year, quarter, curriculum, number, id, is_private: false)
100
+ fix_param(curriculum)
101
+ data = parse("#{endpoint(is_private)}course/#{year},#{quarter}," \
102
+ "#{curriculum},#{number}/#{id}.json")
103
+
104
+ data
105
+ end
106
+
107
+ #
108
+ # these are for UW stff/faculty only, authentcation required
109
+ # other methods that have is_private: as a param option can call
110
+ # the private endpoint as well as the public endpoint
111
+ #
112
+
113
+ def test_score(type, regid)
114
+ parse("#{endpoint(true)}testscore/#{type},#{regid}.json")
115
+ end
116
+
117
+ def enrollment_search(regid, verbose: "")
118
+ data = parse("#{endpoint(true)}enrollment.json?reg_id=#{regid}"\
119
+ "&verbose=#{verbose}")
120
+
121
+ verbose.empty? ? data["EnrollmentLinks"] : data["Enrollments"]
122
+ end
123
+
124
+ def enrollment(year, quarter, regid, verbose: "")
125
+ parse("#{endpoint(true)}enrollment/#{year},#{quarter},#{regid}.json"\
126
+ "?verbose=#{verbose}")
127
+ end
128
+
129
+ def section_status(year, quarter, curric, course, id)
130
+ fix_param(curric)
131
+
132
+ parse("#{endpoint(true)}course/#{year},#{quarter},#{curric}," \
133
+ "#{course}/#{id}/status.json")
134
+ end
135
+
136
+ def term_private(year, quarter)
137
+ parse("#{endpoint(true)}term/#{year},#{quarter}.json")
138
+ end
139
+
140
+ def term_current_private
141
+ parse("#{endpoint(true)}term/current.json")
142
+ end
143
+
144
+ def term_next_private
145
+ parse("#{endpoint(true)}term/next.json")
146
+ end
147
+
148
+ def term_previous_private
149
+ parse("#{endpoint(true)}term/previous.json")
150
+ end
151
+
152
+ def person(regid)
153
+ parse("#{endpoint(true)}person/#{regid}.json")
154
+ end
155
+
156
+ def person_search(type, id)
157
+ parse("#{endpoint(true)}person.json?#{type}=#{id}")
158
+ end
159
+
160
+ def registration(year, quarter, curric, course, id, reg_id, dup_code = "")
161
+ fix_param(curric)
162
+
163
+ parse("#{endpoint(true)}registration/#{year},#{quarter},#{curric}," \
164
+ "#{course},#{id},#{reg_id},#{dup_code}.json")
165
+ end
166
+
167
+ def registration_search(year, quarter, curriculum: "", course: "",
168
+ section: "", reg_id: "", active: "",
169
+ reg_id_instructor: "")
170
+ fix_param(curriculum)
171
+ data = parse("#{endpoint(true)}registration.json?year=#{year}&"\
172
+ "quarter=#{quarter}&curriculum_abbreviation=#{curriculum}&"\
173
+ "course_number=#{course}&section_id=#{section}&"\
174
+ "reg_id=#{reg_id}&is_active=#{active}&"\
175
+ "instructor_reg_id=#{reg_id_instructor}")
176
+
177
+ data["Registrations"]
178
+ end
179
+
180
+ private
181
+
182
+ def default_logger
183
+ @logger = Logger.new(STDOUT)
184
+ @logger.datetime_format = "%Y-%m-%d %H:%M:%S"
185
+ @logger.level = Logger::FATAL
186
+
187
+ @logger
188
+ end
189
+
190
+ def fix_param(param)
191
+ unless param.to_s.empty?
192
+ param.include?(" ") ? param.gsub!(" ", "%20") : param
193
+ param.include?("&") ? param.gsub!("&", "%26") : param
194
+ end
195
+ end
196
+
197
+ def endpoint(is_private = false)
198
+ @private_endpoint = is_private
199
+
200
+ is_private ? @base_private : @base
201
+ end
202
+
203
+ def parse(url)
204
+ data = request(url)
205
+ return nil unless !data.nil?
206
+ data = clean(data)
207
+
208
+ @last = JSON.parse(data)
209
+ @logger.debug "fetched - #{@last}"
210
+ @next = @last["Next"].nil? ? "" : @last["Next"]["Href"]
211
+
212
+ @last
213
+ end
214
+
215
+ def request(url)
216
+ cache_path = Dir.pwd + "/cache/#{url.gsub('/', '')}"
217
+
218
+ data = get_cache(cache_path)
219
+ if data.nil?
220
+ restful_client(url).get do |response, request, result, &block|
221
+ if response.code == 200
222
+ set_cache(response, cache_path)
223
+ data = response
224
+ elsif response.code == 301
225
+ response.follow_redirection(request, result, &block)
226
+ elsif response.code == 401 ||
227
+ (response.code == 500 &&
228
+ response.to_s.include?("Sr-Course-Titles") && !@throw_HEPPS)
229
+ # these should be reported to help@uw.edu
230
+ # HEPPS errors for future courses, report to help@uw.edu
231
+ # HEPPS errors for past courses are not fixable
232
+ @logger.warn("#{url} - #{response.to_s}")
233
+ elsif response.code == 404 && !@throw_404
234
+ @logger.warn("#{url} - 404 - #{response.to_s}")
235
+ else
236
+ raise "Errors for #{url}\n#{response.to_s}"
237
+ end
238
+ end
239
+ end
240
+
241
+ data
242
+ end
243
+
244
+ def restful_client(url, is_private: false)
245
+ if @private_endpoint
246
+ RestClient::Resource.new(
247
+ url,
248
+ ssl_client_cert: OpenSSL::X509::Certificate.new(@cert_file),
249
+ ssl_client_key: OpenSSL::PKey::RSA.new(@key_file),
250
+ log: @logger)
251
+ else
252
+ RestClient::Resource.new(url, log: @logger)
253
+ end
254
+ end
255
+
256
+ def get_cache(file)
257
+ if @use_cache && File.exist?(file)
258
+ @logger.debug "Getting cache for #{file}"
259
+ File.open(file).read
260
+ else
261
+ nil
262
+ end
263
+ end
264
+
265
+ def set_cache(response, url)
266
+ if @use_cache
267
+ @logger.debug "Setting cache for #{url}"
268
+ File.open(url, "w") { |f| f.write(response) }
269
+ end
270
+ end
271
+
272
+ def load_config(cert, key)
273
+ if ! (cert.empty? && key.empty?)
274
+ does_exist?(cert)
275
+ @cert_file = File.read(cert)
276
+ does_exist?(key)
277
+ @key_file = File.read(key)
278
+ @logger.debug "loaded cert and key files"
279
+ end
280
+
281
+ true
282
+ end
283
+
284
+ def does_exist?(file)
285
+ raise "Could not find #{file}" unless File.exist?(file)
286
+ end
287
+
288
+ def clean_bools(data)
289
+ data.gsub('"false"', "false")
290
+ data.gsub('"true"', "true")
291
+ end
292
+
293
+ def clean_spaces(data)
294
+ data.gsub! /(\\?"|)((?:.(?!\1))+.)(?:\1)/ do |match|
295
+ match.gsub(/^(\\?")\s+|\s+(\\?")$/, "\\1\\2").strip
296
+ end
297
+ end
298
+
299
+ def clean(data)
300
+ data = clean_spaces(data)
301
+ data = clean_bools(data)
302
+ end
303
+ end
@@ -0,0 +1,161 @@
1
+ require "minitest/autorun"
2
+ require "json"
3
+ require "logger"
4
+ require_relative "../lib/uw_sws"
5
+
6
+ describe UwSws do
7
+ before do
8
+ log = Logger.new("log.txt")
9
+ log.level = Logger::FATAL
10
+
11
+ cert = "/home/marc/.keys/milesm.bschool.pem"
12
+ key = "/home/marc/.keys/ItsAllGood.key"
13
+ @regid = "DB79E7927ECA11D694790004AC494FFE"
14
+ @uw = UwSws.new(cert: cert, key: key, logger: log, use_cache: true)
15
+ end
16
+
17
+ #
18
+ # all of these test are for private endpoints
19
+ # they require that you initialize the service with a cert
20
+ # and a key file as they are required for all of these tests.
21
+ # Simply delete this test if you want your rake to pass and are
22
+ # not using these endpoints
23
+ #
24
+ describe "when getting test score private endpoint " do
25
+ it "it must not be nil" do
26
+ @uw.test_score("SAT", "9136CCB8F66711D5BE060004AC494FFE")
27
+ @uw.last.wont_be_nil
28
+ end
29
+ end
30
+
31
+ describe "when getting term private endpoint " do
32
+ it "it must not be nil" do
33
+ @uw.term_private(2013, "autumn")
34
+ @uw.last.wont_be_nil
35
+ end
36
+ end
37
+
38
+ describe "when asked for the current, next and previous term private " do
39
+ it "each must respond with a FirstDay" do
40
+ @uw.term_current_private["FirstDay"].wont_be_nil
41
+ @uw.term_next_private["FirstDay"].wont_be_nil
42
+ @uw.term_previous_private["FirstDay"].wont_be_nil
43
+ end
44
+ end
45
+
46
+ describe "when getting section status private endpoint " do
47
+ it "it must not be nil" do
48
+ # this endpoint requires extra permissions that I dont have
49
+ # @uw.section_status(2009, "winter", "MUSAP", 218, "A")
50
+ # @uw.last.wont_be_nil
51
+ end
52
+ end
53
+
54
+ describe "when getting sections private endpoint " do
55
+ it "must return at least 5 of them" do
56
+ @uw.sections(1999, curriculum: "OPMGT", is_private: true)
57
+ .size.must_be :>, 5
58
+ end
59
+ end
60
+
61
+ describe "when getting a section private endpoint " do
62
+ it "must have 8 enrolled" do
63
+ data = @uw.section(1992, "autumn", "CSE", 142, "AA", is_private: true)
64
+ data["CurrentEnrollment"].must_equal("8")
65
+ end
66
+ end
67
+
68
+ describe "when getting a course private endpoint " do
69
+ it "must return course data" do
70
+ data = @uw.course(1992, "autumn", "CSE", 142, is_private: true)
71
+ data["FirstEffectiveTerm"].wont_be_empty
72
+ end
73
+ end
74
+
75
+ describe "when getting a person private endpoint " do
76
+ it "must not be nil" do
77
+ @uw.person(@regid)
78
+ @uw.last.wont_be_nil
79
+ end
80
+ end
81
+
82
+ describe "when doing a search for a person private endpoint " do
83
+ it "each must not be nil" do
84
+ @uw.person_search("reg_id", @regid)
85
+ @uw.last.wont_be_nil
86
+ @uw.person_search("net_id", "milesm")
87
+ @uw.last.wont_be_nil
88
+ @uw.person_search("student_number", "0242267")
89
+ @uw.last.wont_be_nil
90
+ @uw.person_search("employee_id", "864004999")
91
+ @uw.last.wont_be_nil
92
+ end
93
+ end
94
+
95
+ describe "when doing an enrollment search private endpoint " do
96
+ it "it must equal 2" do
97
+ @uw.enrollment_search(@regid).size.must_equal(2)
98
+ end
99
+ end
100
+
101
+ describe "when doing a verbose enrollment search private endpoint " do
102
+ it "it must have 2" do
103
+ @uw.enrollment_search(@regid, verbose: "on").size.must_equal(2)
104
+ end
105
+ end
106
+
107
+ describe "when getting a grade within an enrollment private endpoint " do
108
+ it "it must equal 3.9" do
109
+ data = @uw.enrollment(2002, "summer", @regid, verbose: "on")
110
+ data["Registrations"][0]["Grade"].must_equal("3.9")
111
+ end
112
+ end
113
+
114
+ #
115
+ # NOTE ABOUT REGISTRATION SEARCHES
116
+ # THEY ONLY WORK WITH CURRENT TERMS....
117
+ # these tests will fail unless the params are in the present year/quarter
118
+ #
119
+ describe "when getting a registration private endpoint " do
120
+ it "it must not be nil" do
121
+ # since registrations are not available for prev terms
122
+ # make this a current year and valid regid
123
+ #
124
+ #term = @uw.term_current
125
+ #@uw.registration(term["Year"], term["Quarter"], "CSE", 142, "A",
126
+ # "6ADA93ABA771476481FE44FC086C74DA")
127
+ #@uw.last.wont_be_nil
128
+ end
129
+ end
130
+
131
+ describe "when searching for active course registrations private endpoint " do
132
+ it "it must be greater than 100" do
133
+ term = @uw.term_current
134
+ data = @uw.registration_search(term["Year"], term["Quarter"],
135
+ curriculum: "CSE", course: 142,
136
+ section: "A", active: "on")
137
+ data.size.must_be :>, 100
138
+ end
139
+ end
140
+
141
+ describe "when searching for course registrations private endpoint " do
142
+ it "it must be greater than 200" do
143
+ term = @uw.term_current
144
+ data = @uw.registration_search(term["Year"], term["Quarter"],
145
+ curriculum: "CSE", course: 142,
146
+ section: "A")
147
+ data.size.must_be :>, 200
148
+ end
149
+ end
150
+
151
+ describe "when searching for person registrations private endpoint " do
152
+ it "it must have more than 10" do
153
+ # since registrations are not available for prev terms
154
+ # make this a current year and valid regid
155
+ #
156
+ #@uw.registration_search(2013, "autumn",
157
+ # reg_id: "6ADA93ABA771476481FE44FC086C74DA")
158
+ # .size.must_be :>, 10
159
+ end
160
+ end
161
+ end