pcr-ruby 0.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/.gitignore CHANGED
@@ -17,6 +17,4 @@ spec/reports
17
17
  test/tmp
18
18
  test/version_tmp
19
19
  tmp
20
- instructors.txt
21
- old
22
- pcr-ruby
20
+ instructors.txt
data/README.md CHANGED
@@ -4,11 +4,11 @@ pcr-ruby is a simple, intuitive way to retrieve course data from the Penn Course
4
4
 
5
5
  ## How to use pcr-ruby #
6
6
 
7
- *This section will change a lot as pcr-ruby is developed. As such, this section may not be fully accurate, but I will try to keep the instructions as current as possible.*
7
+ *This section may change a lot as pcr-ruby is developed. As such, this section may not be fully accurate, but I will try to keep the instructions as current as possible.*
8
8
 
9
9
  pcr-ruby follows the structure of the PCR API, with a few name changes to make object identities and roles clearer in your code. (Before using pcr-ruby, you should most definitely read the PCR API documentation, the link to which you should recieve upon being granted your API token.)
10
10
 
11
- The PCR API essentially consists of four types of objects: 'Courses', 'Sections', 'Instructors', and 'Course Histories'. pcr-ruby aims to provide intuitive access to the data contained in these four object types while abstracting you and your user from background processing and unnecessary data. To that end, pcr-ruby (thus far) consists of two types of objects: 'Courses' and 'Sections' ('Instructors' coming soon).
11
+ The PCR API essentially consists of four types of objects: 'Courses', 'Sections', 'Instructors', and 'Course Histories'. pcr-ruby aims to provide intuitive access to the data contained in these four object types while abstracting you and your user from background processing and unnecessary data. To that end, pcr-ruby (thus far) consists of two types of objects: 'Courses', 'Sections', and 'Instructors'.
12
12
 
13
13
  ### 'Courses' in pcr-ruby ###
14
14
 
@@ -16,7 +16,9 @@ Course objects in the PCR API are essentially a group of that Course's Sections
16
16
 
17
17
  To create a Course:
18
18
  ```ruby
19
- course = PCR::Course.new(:course_code => "DEPT-###")
19
+ require 'pcr-ruby'
20
+ pcr = PCR.new(api_token)
21
+ course = pcr.course(course_code)
20
22
  ```
21
23
  All other instance variables will auto-populate based on data from the PCR API.
22
24
 
@@ -38,7 +40,9 @@ In pcr-ruby, Sections are single offerings of a Course. Each Section is associa
38
40
 
39
41
  To create a Section:
40
42
  ```ruby
41
- section = PCR::Section.new(:instance_variable => value)
43
+ require 'pcr-ruby'
44
+ pcr = PCR.new(api_token)
45
+ section = pcr.section(id)
42
46
  ```
43
47
  Possible instance variables available for setting in the Section initialize method are: aliases, id, name, path, semester.
44
48
 
@@ -51,10 +55,8 @@ Sections have the following instance variables:
51
55
  * **description** -- a string containing the class description, which is written by the Section's Instructor and details the scope and characteristics of the class.
52
56
  * **comments** -- a string containing PCR's comments about the Section. The comments are the most major part of the written review, and are sourced from student exit surveys.
53
57
  * **ratings** -- a Hash of metrics and the ratings of the Section for each metric.
54
- * **instructor** (to be developed) -- the Instructor object for the Section's professor.
55
-
56
- Sections have the following instance methods:
57
- * **reviews()** -- retrieves the Section's review data from PCR. Returns a Hash in the format {"comments" => @comments, "ratings" => @ratings}.
58
+ * **instructors** an Array of the Section's instructors.
59
+ * **reviews** -- a Hash of the Section's review data, in the format {"comments" => @comments, "ratings" => @ratings}.
58
60
 
59
61
  ### 'Instructors' in pcr-ruby ###
60
62
 
@@ -62,20 +64,19 @@ Instructors are arguably the most important part of PCR -- many students use PCR
62
64
 
63
65
  To create an Instructor:
64
66
  ```ruby
65
- instructor = PCR::Instructor.new(:id => id [, :name => name, :path => path, :sections => sections])
67
+ require 'pcr-ruby'
68
+ pcr = PCR.new(api_token)
69
+ instructor = pcr.instructor(id)
66
70
  ```
67
- (You really only need to pass the id argument, as pcr-ruby will automatically retrieve the other information from PCR.)
68
71
 
69
- Insctructors have the following instance variables:
70
- * **id** -- the Instructor's PCR id, a String in the form of "ID#-FIRST-M-LAST".
72
+ Insctructors have the following instance variables. All variables other than **id**, which is user-specified, will be filled from the PCR API:
73
+ * **id** -- the Instructor's PCR id, a String in the form of "ID#-FIRST-M-LAST". The PCR API also accepts a String in the form of "ID#".
71
74
  * **name** -- the Instructor's name, a String in the form of "FIRST-M-LAST".
72
75
  * **path** -- the PCR sub-path that leads to the Instructor, a String in the form of "/instructors/id".
73
76
  * **sections** -- a Hash of sections taught by Instructor.
74
77
  * **reviews** -- a Hash of reviews of Instructor in JSON.
75
78
 
76
79
  Instructors have the following instance methods:
77
- * **getInfo** -- a utility method to fill in missing info in Instructor object (used to minimize unnecessary API hits)
78
- * **getReviews** -- a utility method to get review info for Instructor object (used to minimize unnecessary API hits)
79
80
  * **average(metric)** -- returns the average value, across all Sections taught by Instructor, of "metric" as a Float. "Metric" must be a recognized rating in the PCR API. (Currently the names of these ratings are not intuitive, so I may provide plain-English access to rating names in the future.)
80
81
  * **recent(metric)** -- returns the average value of "metric" for the most recent semester in which the Instructor taught as a float. (For example, if the professor taught 3 classes in the most recent semester, this would return the average of "metric" over the three classes.) "Metric" must be a recognized rating in the PCR API. (Currently the names of these ratings are not intuitive, so I may provide plain-English access to rating names in the future.)
81
82
 
@@ -89,7 +90,8 @@ Let's say we want to find the average course quality rating for Introduction to
89
90
  ```ruby
90
91
  require 'pcr.rb'
91
92
  course_code = "PSCI-150"
92
- course = PCR::Course.new(:course_code => course_code)
93
+ pcr = PCR.new(API_TOKEN)
94
+ course = pcr.course(course_code)
93
95
  puts course.average("rCourseQuality") #=> 3.041
94
96
  ```
95
97
 
@@ -97,7 +99,9 @@ Or, even more briefly:
97
99
 
98
100
  ```ruby
99
101
  require 'pcr.rb'
100
- puts PCR::Course.new(:course_code => "PSCI-150").average("rCourseQuality") #=> 3.041
102
+ pcr = PCR.new(API_TOKEN)
103
+ puts pcr.course("PSCI-150")course.average("rCourseQuality")
104
+ #=> 3.041
101
105
  ```
102
106
 
103
107
  ### Get most recent course difficulty rating ###
@@ -105,16 +109,19 @@ Finding the most recent section's course difficulty rating is just as easy:
105
109
 
106
110
  ```ruby
107
111
  require 'pcr.rb'
108
- course = PCR::Course.new(:course_code => "PSCI-150")
112
+ pcr = PCR.new(API_TOKEN)
113
+ course = pcr.course("PSCI-150")
109
114
  puts course.recent("rDifficulty") #=> 2.5
110
115
  ```
111
116
 
112
117
  ### Get professor's average "ability to stimulate student interest" rating ###
113
118
  ```ruby
114
119
  require 'pcr.rb'
115
- instructor = PCR::Instructor.new(:id => "1090-LINDA-H-ZHAO")
120
+ pcr = PCR.new(API_TOKEN)
121
+ instructor = pcr.instructor("1090-LINDA-H-ZHAO")
116
122
  puts instructor.average("rStimulateInterest").round(2) #=> 1.7
117
123
  ```
118
124
 
119
125
  ## TODO ##
126
+ * Implement stricter checks on course code arguments
120
127
  * Implement search by professor last/first name rather than by ID. ID is unintuitive. Will probably need to see if I can make a lookup method, or simply pull down a database of all instructors and do a search on that database.
@@ -0,0 +1,18 @@
1
+ #Add useful array methods
2
+ class Array
3
+ def binary_search(target)
4
+ self.search_iter(0, self.length-1, target)
5
+ end
6
+
7
+ def search_iter(lower, upper, target)
8
+ return -1 if lower > upper
9
+ mid = (lower+upper)/2
10
+ if (self[mid] == target)
11
+ mid
12
+ elsif (target < self[mid])
13
+ self.search_iter(lower, mid-1, target)
14
+ else
15
+ self.search_iter(mid+1, upper, target)
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,89 @@
1
+ #Course object matches up with the coursehistory request of the pcr api.
2
+ #A Course essentially is a signle curriculum and course code, and includes all Sections across time (semesters).
3
+ class Course < PCR
4
+ attr_accessor :course_code, :sections, :id, :name, :path, :reviews
5
+
6
+ def initialize(course_code)
7
+ if course_code.is_a? String and course_code.isValidCourseCode?
8
+ @course_code = course_code
9
+
10
+ #Read JSON from the PCR API
11
+ api_url = @@api_endpt + "coursehistories/" + self.course_code + "/?token=" + @@token
12
+ json = JSON.parse(open(api_url).read)
13
+
14
+ #Create array of Section objects, containing all Sections found in the API JSON for the Course
15
+ @sections = []
16
+ json["result"]["courses"].each do |c|
17
+ @sections << Section.new(c["id"])
18
+ end
19
+
20
+ #Set variables according to Course JSON data
21
+ @id = json["result"]["id"]
22
+ @name = json["result"]["name"]
23
+ @path = json["result"]["path"]
24
+
25
+ #Get reviews for the Course -- unfortunately this has to be a separate query
26
+ api_url_reviews = @@api_endpt + "coursehistories/" + self.id.to_s + "/reviews?token=" + @@token
27
+ json_reviews = JSON.parse(open(api_url_reviews).read)
28
+ @reviews = json_reviews["result"]["values"]
29
+
30
+ else
31
+ raise CourseError, "Invalid course code specified. Use format [DEPT-###]."
32
+ end
33
+ end
34
+
35
+ def average(metric)
36
+ #Ensure that we know argument type
37
+ if metric.is_a? Symbol
38
+ metric = metric.to_s
39
+ end
40
+
41
+ if metric.is_a? String
42
+ #Loop vars
43
+ total = 0
44
+ n = 0
45
+
46
+ #For each section, check if ratings include metric arg -- if so, add metric rating to total and increment counting variable
47
+ self.reviews.each do |review|
48
+ ratings = review["ratings"]
49
+ if ratings.include? metric
50
+ total = total + review["ratings"][metric].to_f
51
+ n = n + 1
52
+ else
53
+ raise CourseError, "No ratings found for \"#{metric}\" in #{self.name}."
54
+ end
55
+ end
56
+
57
+ #Return average score as a float
58
+ (total/n)
59
+
60
+ else
61
+ raise CourseError, "Invalid metric format. Metric must be a string or symbol."
62
+ end
63
+ end
64
+
65
+ def recent(metric)
66
+ #Ensure that we know argument type
67
+ if metric.is_a? Symbol
68
+ metric = metric.to_s
69
+ end
70
+
71
+
72
+ if metric.is_a? String
73
+ #Get the most recent section
74
+ section = self.sections[-1]
75
+
76
+ #Iterate through all the section reviews, and if the section review id matches the id of the most recent section, return that rating
77
+ self.reviews.each do |review|
78
+ if review["section"]["id"].to_s[0..4].to_i == section.id
79
+ return review["ratings"][metric]
80
+ end
81
+ end
82
+
83
+ raise CourseError, "No ratings found for #{metric} in #{section.semester}."
84
+
85
+ else
86
+ raise CourseError, "Invalid metric format. Metric must be a string or symbol."
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,6 @@
1
+ #These errors serve as more specific exceptions so we know where exactly errors are coming from.
2
+ class CourseError < StandardError
3
+ end
4
+
5
+ class InstructorError < StandardError
6
+ end
@@ -0,0 +1,130 @@
1
+ #Instructor is a professor. Instructors are not tied to a course or section, but will have to be referenced from Sections.
2
+ class Instructor < PCR
3
+ attr_accessor :id, :name, :path, :sections, :reviews
4
+
5
+ def initialize(id)
6
+ #Assign args. ID is necessary because that's how we look up Instructors in the PCR API.
7
+ if id.is_a? String
8
+ @id = id
9
+ else
10
+ raise InstructorError("Invalid Instructor ID specified.")
11
+ end
12
+
13
+ #Hit PCR API to get missing info based on id
14
+ self.getInfo
15
+ self.getReviews
16
+
17
+ end
18
+
19
+ #Hit the PCR API to get all missing info
20
+ #Separate method in case we want to conduct it separately from a class init
21
+ def getInfo
22
+ api_url = @@api_endpt + "instructors/" + self.id + "?token=" + @@token
23
+ json = JSON.parse(open(api_url).read)
24
+
25
+ @name = json["result"]["name"].downcase.titlecase unless @name
26
+ @path = json["result"]["path"] unless @path
27
+ @sections = json["result"]["reviews"]["values"] unless @sections #Mislabeled reviews in PCR API
28
+ end
29
+
30
+ #Separate method for getting review data in case we don't want to make an extra API hit each init
31
+ def getReviews
32
+ if not self.reviews #make sure we don't already have reviews
33
+ api_url = @@api_endpt + "instructors/" + self.id + "/reviews?token=" + @@token
34
+ json = JSON.parse(open(api_url).read)
35
+
36
+ @reviews = json["result"]["values"] #gets array
37
+ end
38
+ end
39
+
40
+ #Get average value of a certain rating for Instructor
41
+ def average(metric)
42
+ #Ensure that we know argument type
43
+ if metric.is_a? Symbol
44
+ metric = metric.to_s
45
+ end
46
+
47
+ if metric.is_a? String
48
+ #Loop vars
49
+ total = 0
50
+ n = 0
51
+
52
+ #For each section, check if ratings include metric arg -- if so, add metric rating to total and increment counting variable
53
+ self.getReviews
54
+ self.reviews.each do |review|
55
+ ratings = review["ratings"]
56
+ if ratings.include? metric
57
+ total = total + review["ratings"][metric].to_f
58
+ n = n + 1
59
+ else
60
+ raise CourseError, "No ratings found for \"#{metric}\" for #{self.name}."
61
+ end
62
+ end
63
+
64
+ #Return average score as a float
65
+ (total/n)
66
+
67
+ else
68
+ raise CourseError, "Invalid metric format. Metric must be a string or symbol."
69
+ end
70
+ end
71
+
72
+ #Get most recent value of a certain rating for Instructor
73
+ def recent(metric)
74
+ #Ensure that we know argument type
75
+ if metric.is_a? Symbol
76
+ metric = metric.to_s
77
+ end
78
+
79
+ if metric.is_a? String
80
+ #Iterate through reviews and create Section for each section reviewed, presented in an array
81
+ sections = []
82
+ section_ids = []
83
+ self.getReviews
84
+ self.reviews.each do |review|
85
+ if section_ids.index(review["section"]["id"].to_i).nil?
86
+ s = PCR::Section.new(review["section"]["id"].to_i, false)
87
+ sections << s
88
+ section_ids << s.id
89
+ end
90
+ end
91
+
92
+ #Get only most recent Section(s) in the array
93
+ sections.reverse! #Newest first
94
+ targets = []
95
+ sections.each do |s|
96
+ s.hit_api(:get_reviews => true)
97
+ if sections.index(s) == 0
98
+ targets << s
99
+ elsif s.semester == sections[0].semester and s.id != sections[0].id
100
+ targets << s
101
+ else
102
+ break
103
+ end
104
+ end
105
+
106
+ #Calculate recent rating
107
+ total = 0
108
+ num = 0
109
+ targets.each do |section|
110
+ #Make sure we get the rating for the right Instructor
111
+ section.ratings.each do |rating|
112
+ if rating.key?(self.id)
113
+ if rating[self.id][metric].nil?
114
+ raise InstructorError, "No ratings found for #{metric} for #{self.name}."
115
+ else
116
+ total = total + rating[self.id][metric].to_f
117
+ num += 1
118
+ end
119
+ end
120
+ end
121
+ end
122
+
123
+ # Return recent rating
124
+ total / num
125
+
126
+ else
127
+ raise CourseError, "Invalid metric format. Metric must be a string or symbol."
128
+ end
129
+ end
130
+ end
@@ -0,0 +1,53 @@
1
+ #Section is an individual class under the umbrella of a general Course
2
+ class Section < PCR
3
+ attr_accessor :aliases, :id, :name, :path, :semester, :description, :comments, :ratings, :instructors, :reviews
4
+
5
+ def initialize(id, hit_api = true)
6
+ # Set instance vars
7
+ @id = id
8
+
9
+ # Hit api to fill additional info
10
+ self.hit_api() unless hit_api == false
11
+
12
+ end
13
+
14
+ def hit_api()
15
+ data = ["aliases", "name", "path", "semester", "description"]
16
+ api_url = @@api_endpt + "courses/" + self.id.to_s + "?token=" + @@token
17
+ json = JSON.parse(open(api_url).read)
18
+
19
+ data.each do |d|
20
+ case d
21
+ when "aliases"
22
+ self.instance_variable_set("@#{d}", json["result"]["aliases"])
23
+ when "name"
24
+ self.instance_variable_set("@#{d}", json["result"]["name"])
25
+ when "path"
26
+ self.instance_variable_set("@#{d}", json["result"]["path"])
27
+ when "semester"
28
+ self.instance_variable_set("@#{d}", json["result"]["semester"])
29
+ when "description"
30
+ self.instance_variable_set("@#{d}", json["result"]["description"])
31
+ end
32
+ end
33
+
34
+ # Get review data
35
+ self.get_reviews
36
+
37
+ end
38
+
39
+ def get_reviews()
40
+ api_url = @@api_endpt + "courses/" + self.id.to_s + "/reviews?token=" + @@token
41
+ json = JSON.parse(open(api_url).read)
42
+ @comments = []
43
+ @ratings = []
44
+ @instructors = []
45
+ json["result"]["values"].each do |a|
46
+ @comments << {a["instructor"]["id"] => a["comments"]}
47
+ @ratings << {a["instructor"]["id"] => a["ratings"]}
48
+ @instructors << a["instructor"]
49
+ end
50
+ @reviews = {"comments" => @comments, "ratings" => @ratings}
51
+ end
52
+
53
+ end
@@ -0,0 +1,68 @@
1
+ #Add some useful String methods
2
+ class ::String
3
+ #Checks if String is valid Penn course code format
4
+ def isValidCourseCode?
5
+ test = self.split('-')
6
+ if test[0].length == 4 and test[1].length == 3
7
+ true
8
+ else
9
+ false
10
+ end
11
+ end
12
+
13
+ #Methods to convert strings to titlecase.
14
+ #Thanks https://github.com/samsouder/titlecase
15
+ def titlecase
16
+ small_words = %w(a an and as at but by en for if in of on or the to v v. via vs vs.)
17
+
18
+ x = split(" ").map do |word|
19
+ # note: word could contain non-word characters!
20
+ # downcase all small_words, capitalize the rest
21
+ small_words.include?(word.gsub(/\W/, "").downcase) ? word.downcase! : word.smart_capitalize!
22
+ word
23
+ end
24
+ # capitalize first and last words
25
+ x.first.smart_capitalize!
26
+ x.last.smart_capitalize!
27
+ # small words after colons are capitalized
28
+ x.join(" ").gsub(/:\s?(\W*#{small_words.join("|")}\W*)\s/) { ": #{$1.smart_capitalize} " }
29
+ end
30
+
31
+ def smart_capitalize
32
+ # ignore any leading crazy characters and capitalize the first real character
33
+ if self =~ /^['"\(\[']*([a-z])/
34
+ i = index($1)
35
+ x = self[i,self.length]
36
+ # word with capitals and periods mid-word are left alone
37
+ self[i,1] = self[i,1].upcase unless x =~ /[A-Z]/ or x =~ /\.\w+/
38
+ end
39
+ self
40
+ end
41
+
42
+ def smart_capitalize!
43
+ replace(smart_capitalize)
44
+ end
45
+
46
+ #Method to compare semesters. Returns true if self is later, false if self is before, 0 if same
47
+ #s should be a string like "2009A"
48
+ def compareSemester(s)
49
+ year = self[0..3]
50
+ season = self[4]
51
+ compYear = s[0..3]
52
+ compSeason = s[4]
53
+
54
+ if year.to_i > compYear.to_i #Later year
55
+ return true
56
+ elsif year.to_i < compYear.to_i #Earlier year
57
+ return false
58
+ elsif year.to_i == compYear.to_i #Same year, so test season
59
+ if season > compSeason #Season is later
60
+ return true
61
+ elsif season = compSeason #Exact same time
62
+ return 0
63
+ elsif season < compSeason #compSeason is later
64
+ return false
65
+ end
66
+ end
67
+ end
68
+ end
@@ -15,12 +15,12 @@ class PCR
15
15
  Course.new(course_code)
16
16
  end
17
17
 
18
- def section(*args)
19
- Section.new(*args)
18
+ def section(id, hit_api = true)
19
+ Section.new(id, hit_api)
20
20
  end
21
21
 
22
- def instructor(id, *args)
23
- Instructor.new(id, *args)
22
+ def instructor(id)
23
+ Instructor.new(id)
24
24
  end
25
25
  end
26
26
 
@@ -0,0 +1,5 @@
1
+ module Pcr
2
+ module Ruby
3
+ VERSION = "0.2"
4
+ end
5
+ end
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.1'
4
+ version: '0.2'
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2012-10-21 00:00:00.000000000 Z
12
+ date: 2012-10-22 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: json
@@ -38,7 +38,14 @@ files:
38
38
  - LICENSE
39
39
  - README.md
40
40
  - Rakefile
41
+ - lib/classes/array.rb
42
+ - lib/classes/course.rb
43
+ - lib/classes/errors.rb
44
+ - lib/classes/instructor.rb
45
+ - lib/classes/section.rb
46
+ - lib/classes/string.rb
41
47
  - lib/pcr-ruby.rb
48
+ - lib/pcr-ruby/version.rb
42
49
  - pcr-ruby.gemspec
43
50
  homepage: ''
44
51
  licenses: []