pcr-ruby 0.0.3 → 0.1

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.
Files changed (2) hide show
  1. data/lib/pcr-ruby.rb +20 -526
  2. metadata +1 -1
@@ -3,532 +3,26 @@ require 'open-uri'
3
3
  require 'time'
4
4
  require 'csv'
5
5
 
6
- module PCR
7
6
 
8
- #Add some useful String methods
9
- class ::String
10
- #Checks if String is valid Penn course code format
11
- def isValidCourseCode?
12
- test = self.split('-')
13
- if test[0].length == 4 and test[1].length == 3
14
- true
15
- else
16
- false
17
- end
18
- end
19
-
20
- #Methods to convert strings to titlecase.
21
- #Thanks https://github.com/samsouder/titlecase
22
- def titlecase
23
- small_words = %w(a an and as at but by en for if in of on or the to v v. via vs vs.)
24
-
25
- x = split(" ").map do |word|
26
- # note: word could contain non-word characters!
27
- # downcase all small_words, capitalize the rest
28
- small_words.include?(word.gsub(/\W/, "").downcase) ? word.downcase! : word.smart_capitalize!
29
- word
30
- end
31
- # capitalize first and last words
32
- x.first.smart_capitalize!
33
- x.last.smart_capitalize!
34
- # small words after colons are capitalized
35
- x.join(" ").gsub(/:\s?(\W*#{small_words.join("|")}\W*)\s/) { ": #{$1.smart_capitalize} " }
36
- end
37
-
38
- def smart_capitalize
39
- # ignore any leading crazy characters and capitalize the first real character
40
- if self =~ /^['"\(\[']*([a-z])/
41
- i = index($1)
42
- x = self[i,self.length]
43
- # word with capitals and periods mid-word are left alone
44
- self[i,1] = self[i,1].upcase unless x =~ /[A-Z]/ or x =~ /\.\w+/
45
- end
46
- self
47
- end
48
-
49
- def smart_capitalize!
50
- replace(smart_capitalize)
51
- end
52
-
53
- #Method to compare semesters. Returns true if self is later, false if self is before, 0 if same
54
- #s should be a string like "2009A"
55
- def compareSemester(s)
56
- year = self[0..3]
57
- season = self[4]
58
- compYear = s[0..3]
59
- compSeason = s[4]
60
-
61
- if year.to_i > compYear.to_i #Later year
62
- return true
63
- elsif year.to_i < compYear.to_i #Earlier year
64
- return false
65
- elsif year.to_i == compYear.to_i #Same year, so test season
66
- if season > compSeason #Season is later
67
- return true
68
- elsif season = compSeason #Exact same time
69
- return 0
70
- elsif season < compSeason #compSeason is later
71
- return false
72
- end
73
- end
74
- end
75
- end
76
-
77
- #Add useful array methods
78
- class Array
79
- def binary_search(target)
80
- self.search_iter(0, self.length-1, target)
81
- end
82
-
83
- def search_iter(lower, upper, target)
84
- return -1 if lower > upper
85
- mid = (lower+upper)/2
86
- if (self[mid] == target)
87
- mid
88
- elsif (target < self[mid])
89
- self.search_iter(lower, mid-1, target)
90
- else
91
- self.search_iter(mid+1, upper, target)
92
- end
93
- end
94
- end
95
-
96
- #API class handles token and api url, so both are easily changed
97
- class API
98
- attr_accessor :token, :api_endpt
99
- def initialize(token)
100
- @token = token
101
- @api_endpt = "http://api.penncoursereview.com/v1/"
102
- end
103
-
104
- def course(args)
105
- Course.new(args)
106
- end
107
-
108
- def section(args)
109
- Section.new(args)
110
- end
111
-
112
- def instructor(id, args)
113
- Instructor.new(id, args)
114
- end
115
- end
116
-
117
- #These errors serve as more specific exceptions so we know where exactly errors are coming from.
118
- class CourseError < StandardError
119
- end
120
-
121
- class InstructorError < StandardError
122
- end
123
-
124
- #Course object matches up with the coursehistory request of the pcr api.
125
- #A Course essentially is a signle curriculum and course code, and includes all Sections across time (semesters).
126
- class Course
127
- attr_accessor :course_code, :sections, :id, :name, :path, :reviews
128
-
129
- def initialize(args)
130
- #Set indifferent access for args hash
131
- args.default_proc = proc do |h, k|
132
- case k
133
- when String then sym = k.to_sym; h[sym] if h.key?(sym)
134
- when Symbol then str = k.to_s; h[str] if h.key?(str)
135
- end
136
- end
137
-
138
- #Initialization actions
139
- if args[:course_code].is_a? String and args[:course_code].isValidCourseCode?
140
- @course_code = args[:course_code]
141
-
142
- #Read JSON from the PCR API
143
- pcr = PCR::API.new()
144
- api_url = pcr.api_endpt + "coursehistories/" + self.course_code + "/?token=" + pcr.token
145
- json = JSON.parse(open(api_url).read)
146
-
147
- #Create array of Section objects, containing all Sections found in the API JSON for the Course
148
- @sections = []
149
- json["result"]["courses"].each do |c|
150
- @sections << Section.new(:aliases => c["aliases"], :id => c["id"], :name => c["name"], :path => c["path"], :semester => c["semester"], :hit_api => true)
151
- end
152
-
153
- #Set variables according to Course JSON data
154
- @id = json["result"]["id"]
155
- @name = json["result"]["name"]
156
- @path = json["result"]["path"]
157
-
158
- #Get reviews for the Course -- unfortunately this has to be a separate query
159
- api_url_reviews = pcr.api_endpt + "coursehistories/" + self.id.to_s + "/reviews?token=" + pcr.token
160
- json_reviews = JSON.parse(open(api_url_reviews).read)
161
- @reviews = json_reviews["result"]["values"]
162
-
163
- else
164
- raise CourseError, "Invalid course code specified. Use format [DEPT-###]."
165
- end
166
- end
167
-
168
- def average(metric)
169
- #Ensure that we know argument type
170
- if metric.is_a? Symbol
171
- metric = metric.to_s
172
- end
173
-
174
- if metric.is_a? String
175
- #Loop vars
176
- total = 0
177
- n = 0
178
-
179
- #For each section, check if ratings include metric arg -- if so, add metric rating to total and increment counting variable
180
- self.reviews.each do |review|
181
- ratings = review["ratings"]
182
- if ratings.include? metric
183
- total = total + review["ratings"][metric].to_f
184
- n = n + 1
185
- else
186
- raise CourseError, "No ratings found for \"#{metric}\" in #{self.name}."
187
- end
188
- end
189
-
190
- #Return average score as a float
191
- return (total/n)
192
-
193
- else
194
- raise CourseError, "Invalid metric format. Metric must be a string or symbol."
195
- end
196
- end
197
-
198
- def recent(metric)
199
- #Ensure that we know argument type
200
- if metric.is_a? Symbol
201
- metric = metric.to_s
202
- end
203
-
204
-
205
- if metric.is_a? String
206
- #Get the most recent section
207
- section = self.sections[-1]
208
-
209
- #Iterate through all the section reviews, and if the section review id matches the id of the most recent section, return that rating
210
- self.reviews.each do |review|
211
- if review["section"]["id"].to_s[0..4].to_i == section.id
212
- return review["ratings"][metric]
213
- end
214
- end
215
-
216
- raise CourseError, "No ratings found for #{metric} in #{section.semester}."
217
-
218
- else
219
- raise CourseError, "Invalid metric format. Metric must be a string or symbol."
220
- end
221
- end
222
- end
223
-
224
- #Section is an individual class under the umbrella of a general Course
225
- class Section
226
- attr_accessor :aliases, :id, :name, :path, :semester, :description, :comments, :ratings, :instructor
227
-
228
- def initialize(args)
229
- #Set indifferent access for args
230
- args.default_proc = proc do |h, k|
231
- case k
232
- when String then sym = k.to_sym; h[sym] if h.key?(sym)
233
- when Symbol then str = k.to_s; h[str] if h.key?(str)
234
- end
235
- end
236
-
237
- pcr = PCR::API.new()
238
- @aliases = args[:aliases] if args[:aliases].is_a? Array
239
- @id = args[:id] if args[:id].is_a? Integer
240
- @name = args[:name] if args[:name].is_a? String
241
- @path = args[:path] if args[:path].is_a? String
242
- @semester = args[:semester] if args[:semester].is_a? String
243
- @comments = ""
244
- @ratings = {}
245
- @instructor = {}
246
-
247
- if args[:hit_api]
248
- if args[:get_reviews]
249
- self.hit_api(:get_reviews => true)
250
- else
251
- self.hit_api(:get_reviews => false)
252
- end
253
- end
254
- end
255
-
256
- def hit_api(args)
257
- data = ["aliases", "name", "path", "semester", "description"]
258
- pcr = PCR::API.new()
259
- api_url = pcr.api_endpt + "courses/" + self.id.to_s + "?token=" + pcr.token
260
- json = JSON.parse(open(api_url).read)
261
-
262
- data.each do |d|
263
- case d
264
- when "aliases"
265
- self.instance_variable_set("@#{d}", json["result"]["aliases"])
266
- when "name"
267
- self.instance_variable_set("@#{d}", json["result"]["name"])
268
- when "path"
269
- self.instance_variable_set("@#{d}", json["result"]["path"])
270
- when "semester"
271
- self.instance_variable_set("@#{d}", json["result"]["semester"])
272
- when "description"
273
- self.instance_variable_set("@#{d}", json["result"]["description"])
274
- end
275
- end
276
-
277
- if args[:get_reviews]
278
- self.reviews()
279
- end
280
- end
281
-
282
- def reviews()
283
- pcr = PCR::API.new()
284
- api_url = pcr.api_endpt + "courses/" + self.id.to_s + "/reviews?token=" + pcr.token
285
- json = JSON.parse(open(api_url).read)
286
- @comments = []
287
- @ratings = []
288
- @instructors = []
289
- json["result"]["values"].each do |a|
290
- @comments << {a["instructor"]["id"] => a["comments"]}
291
- @ratings << {a["instructor"]["id"] => a["ratings"]}
292
- @instructors << a["instructor"]
293
- end
294
- # @comments = json["result"]["values"][0]["comments"]
295
- # @ratings = json["result"]["values"][0]["ratings"]
296
- # @instructor = json["result"]["values"][0]["instructor"]
297
-
298
- return {:comments => @comments, :ratings => @ratings}
299
- end
300
-
301
- def after(s)
302
- if s.is_a? Section
303
- self.semester.compareSemester(s.semester)
304
- elsif s.is_a? String
305
- self.semester.compareSemester(s)
306
- end
307
- end
308
- end
309
-
310
- #Instructor is a professor. Instructors are not tied to a course or section, but will have to be referenced from Sections.
311
- class Instructor
312
- attr_accessor :id, :name, :path, :sections, :reviews
313
-
314
- def initialize(id, args)
315
- #Set indifferent access for args
316
- args.default_proc = proc do |h, k|
317
- case k
318
- when String then sym = k.to_sym; h[sym] if h.key?(sym)
319
- when Symbol then str = k.to_s; h[str] if h.key?(str)
320
- end
321
- end
322
-
323
- #Assign args. ID is necessary because that's how we look up Instructors in the PCR API.
324
- if id.is_a? String
325
- @id = id
326
- else
327
- raise InstructorError("Invalid Instructor ID specified.")
328
- end
329
-
330
- @name = args[:name].downcase.titlecase if args[:name].is_a? String
331
- @path = args[:path] if args[:path].is_a? String
332
- @sections = args[:sections] if args[:sections].is_a? Hash
333
-
334
- #Hit PCR API to get missing info, if requested
335
- if args[:hit_api] == true
336
- self.getInfo
337
- self.getReviews
338
- end
339
- end
340
-
341
- #Hit the PCR API to get all missing info
342
- #Separate method in case we want to conduct it separately from a class init
343
- def getInfo
344
- pcr = PCR::API.new()
345
- api_url = pcr.api_endpt + "instructors/" + self.id + "?token=" + pcr.token
346
- json = JSON.parse(open(api_url).read)
347
-
348
- @name = json["result"]["name"].downcase.titlecase unless @name
349
- @path = json["result"]["path"] unless @path
350
- @sections = json["result"]["reviews"]["values"] unless @sections #Mislabeled reviews in PCR API
351
- end
352
-
353
- #Separate method for getting review data in case we don't want to make an extra API hit each init
354
- def getReviews
355
- if not self.reviews #make sure we don't already have reviews
356
- pcr = PCR::API.new()
357
- api_url = pcr.api_endpt + "instructors/" + self.id + "/reviews?token=" + pcr.token
358
- json = JSON.parse(open(api_url).read)
359
-
360
- @reviews = json["result"]["values"] #gets array
361
- end
362
- end
363
-
364
- #Get average value of a certain rating for Instructor
365
- def average(metric)
366
- #Ensure that we know argument type
367
- if metric.is_a? Symbol
368
- metric = metric.to_s
369
- end
370
-
371
- if metric.is_a? String
372
- #Loop vars
373
- total = 0
374
- n = 0
375
-
376
- #For each section, check if ratings include metric arg -- if so, add metric rating to total and increment counting variable
377
- self.getReviews
378
- self.reviews.each do |review|
379
- ratings = review["ratings"]
380
- if ratings.include? metric
381
- total = total + review["ratings"][metric].to_f
382
- n = n + 1
383
- else
384
- raise CourseError, "No ratings found for \"#{metric}\" for #{self.name}."
385
- end
386
- end
387
-
388
- #Return average score as a float
389
- return (total/n)
390
-
391
- else
392
- raise CourseError, "Invalid metric format. Metric must be a string or symbol."
393
- end
394
- end
395
-
396
- #Get most recent value of a certain rating for Instructor
397
- def recent(metric)
398
- #Ensure that we know argument type
399
- if metric.is_a? Symbol
400
- metric = metric.to_s
401
- end
402
-
403
- if metric.is_a? String
404
- #Iterate through reviews and create Section for each section reviewed, presented in an array
405
- sections = []
406
- section_ids = []
407
- self.getReviews
408
- self.reviews.each do |review|
409
- if section_ids.index(review["section"]["id"].to_i).nil?
410
- s = PCR::Section.new(:id => review["section"]["id"].to_i, :hit_api => false)
411
- sections << s
412
- section_ids << s.id
413
- end
414
- end
415
-
416
- #Get only most recent Section(s) in the array
417
- sections.reverse! #Newest first
418
- targets = []
419
- sections.each do |s|
420
- s.hit_api(:get_reviews => true)
421
- if sections.index(s) == 0
422
- targets << s
423
- elsif s.semester == sections[0].semester and s.id != sections[0].id
424
- targets << s
425
- else
426
- break
427
- end
428
- end
429
-
430
- #Calculate recent rating
431
- total = 0
432
- num = 0
433
- targets.each do |section|
434
- #Make sure we get the rating for the right Instructor
435
- section.ratings.each do |rating|
436
- if rating.key?(self.id)
437
- if rating[self.id][metric].nil?
438
- raise InstructorError, "No ratings found for #{metric} for #{self.name}."
439
- else
440
- total = total + rating[self.id][metric].to_f
441
- num += 1
442
- end
443
- end
444
- end
445
- end
446
-
447
- return total / num
448
-
449
- else
450
- raise CourseError, "Invalid metric format. Metric must be a string or symbol."
451
- end
452
- end
453
- end
454
-
455
- end #module
456
-
457
- #Some instance methods to handle instructor searching
458
- def downloadInstructors(instructors_db)
459
- pcr = PCR::API.new()
460
- api_url = pcr.api_endpt + "instructors/" + "?token=" + pcr.token
461
- #puts "Downloading instructors json..."
462
- json = JSON.parse(open(api_url).read)
463
-
464
- #Parse api data, writing to file
465
- begin
466
- File.open(instructors_db, 'w') do |f|
467
- instructor_hashes = json["result"]["values"]
468
- file_lines = []
469
- #puts "Constructing instructor file_lines"
470
- instructor_hashes.each do |instructor|
471
- n = instructor["name"].split(" ")
472
- file_lines << ["#{n[2]} #{n[1]} #{n[0]}",instructor["id"]] if n.length == 3 #if instructor has middle name
473
- file_lines << ["#{n[1]} #{n[0]}",instructor["id"]] if n.length == 2 #if instructor does not have middle name
474
- end
475
-
476
- #Sort lines alphabetically
477
- #puts "sorting file lines alphabetically..."
478
- file_lines.sort! { |a,b| a[0] <=> b[0] }
479
-
480
- #Write lines to csv file
481
- #puts "writing file lines to csv file..."
482
- file_lines.each { |line| f.write("#{line[0]},#{line[1]}\n") }
483
- end
484
- rescue IOError => e
485
- puts "Could not write to instructors file"
486
- rescue Errno::ENOENT => e
487
- puts "Could not open instructors file"
488
- end
7
+ #PCR class handles token and api url, so both are easily changed
8
+ class PCR
9
+ def initialize(token, api_endpt = "http://api.penncoursereview.com/v1/")
10
+ @@token = token
11
+ @@api_endpt = api_endpt
12
+ end
13
+
14
+ def course(course_code)
15
+ Course.new(course_code)
16
+ end
17
+
18
+ def section(*args)
19
+ Section.new(*args)
20
+ end
21
+
22
+ def instructor(id, *args)
23
+ Instructor.new(id, *args)
24
+ end
489
25
  end
490
26
 
491
- def instructorSearch(args)
492
- #Set indifferent access for args
493
- args.default_proc = proc do |h, k|
494
- case k
495
- when String then sym = k.to_sym; h[sym] if h.key?(sym)
496
- when Symbol then str = k.to_s; h[str] if h.key?(str)
497
- end
498
- end
499
-
500
- #Set args
501
- first_name = args[:first_name]
502
- middle_initial = args[:middle_initial]
503
- last_name = args[:last_name]
504
-
505
- #Check if we've downloaded instructors in last week
506
- begin
507
- last_dl_time = Time.local(File.mtime("instructors.txt").tv_sec).tv_sec
508
- #puts last_dl_time
509
- rescue Errno::ENOENT => e
510
- downloadInstructors("instructors.txt") #instructors file doesn't exist, so download
511
- else
512
- current_time = Time.local(Time.now().tv_sec).tv_sec
513
- #puts current_time
514
- if current_time - last_dl_time <= 604800 #1 week in seconds
515
- downloadInstructors("instructors.txt")
516
- end
517
- end
518
-
519
- #Check if instructors file exists
520
- # begin
521
- # f = File.open("instructors.txt", "rb")
522
- # rescue Errno::ENOENT => e
523
- # downloadInstructors("instructors.txt")
524
- # end
525
-
526
- #Search for instructor name in instructors file and get corresponding ids, in an array
527
- #puts "searching instructors file..."
528
- results = []
529
- CSV.foreach("instructors.txt") do |line|
530
- results << {line[0] => line[1]} if line[0].include? last_name.upcase
531
- end
532
-
533
- return results
534
- end
27
+ # Load classes
28
+ Dir[File.dirname(__FILE__) + "/classes/*.rb"].each { |file| require file }
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: pcr-ruby
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.3
4
+ version: '0.1'
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors: