lib/pcr-ruby.rb in pcr-ruby-0.0.3 vs lib/pcr-ruby.rb in pcr-ruby-0.1
- old
+ new
@@ -1,534 +1,28 @@
require 'json'
require 'open-uri'
require 'time'
require 'csv'
-module PCR
- #Add some useful String methods
- class ::String
- #Checks if String is valid Penn course code format
- def isValidCourseCode?
- test = self.split('-')
- if test[0].length == 4 and test[1].length == 3
- true
- else
- false
- end
- end
-
- #Methods to convert strings to titlecase.
- #Thanks https://github.com/samsouder/titlecase
- def titlecase
- small_words = %w(a an and as at but by en for if in of on or the to v v. via vs vs.)
-
- x = split(" ").map do |word|
- # note: word could contain non-word characters!
- # downcase all small_words, capitalize the rest
- small_words.include?(word.gsub(/\W/, "").downcase) ? word.downcase! : word.smart_capitalize!
- word
- end
- # capitalize first and last words
- x.first.smart_capitalize!
- x.last.smart_capitalize!
- # small words after colons are capitalized
- x.join(" ").gsub(/:\s?(\W*#{small_words.join("|")}\W*)\s/) { ": #{$1.smart_capitalize} " }
- end
-
- def smart_capitalize
- # ignore any leading crazy characters and capitalize the first real character
- if self =~ /^['"\(\[']*([a-z])/
- i = index($1)
- x = self[i,self.length]
- # word with capitals and periods mid-word are left alone
- self[i,1] = self[i,1].upcase unless x =~ /[A-Z]/ or x =~ /\.\w+/
- end
- self
- end
-
- def smart_capitalize!
- replace(smart_capitalize)
- end
-
- #Method to compare semesters. Returns true if self is later, false if self is before, 0 if same
- #s should be a string like "2009A"
- def compareSemester(s)
- year = self[0..3]
- season = self[4]
- compYear = s[0..3]
- compSeason = s[4]
-
- if year.to_i > compYear.to_i #Later year
- return true
- elsif year.to_i < compYear.to_i #Earlier year
- return false
- elsif year.to_i == compYear.to_i #Same year, so test season
- if season > compSeason #Season is later
- return true
- elsif season = compSeason #Exact same time
- return 0
- elsif season < compSeason #compSeason is later
- return false
- end
- end
- end
- end
-
- #Add useful array methods
- class Array
- def binary_search(target)
- self.search_iter(0, self.length-1, target)
- end
-
- def search_iter(lower, upper, target)
- return -1 if lower > upper
- mid = (lower+upper)/2
- if (self[mid] == target)
- mid
- elsif (target < self[mid])
- self.search_iter(lower, mid-1, target)
- else
- self.search_iter(mid+1, upper, target)
- end
- end
- end
-
- #API class handles token and api url, so both are easily changed
- class API
- attr_accessor :token, :api_endpt
- def initialize(token)
- @token = token
- @api_endpt = "http://api.penncoursereview.com/v1/"
- end
-
- def course(args)
- Course.new(args)
- end
-
- def section(args)
- Section.new(args)
- end
-
- def instructor(id, args)
- Instructor.new(id, args)
- end
- end
-
- #These errors serve as more specific exceptions so we know where exactly errors are coming from.
- class CourseError < StandardError
- end
-
- class InstructorError < StandardError
- end
-
- #Course object matches up with the coursehistory request of the pcr api.
- #A Course essentially is a signle curriculum and course code, and includes all Sections across time (semesters).
- class Course
- attr_accessor :course_code, :sections, :id, :name, :path, :reviews
-
- def initialize(args)
- #Set indifferent access for args hash
- args.default_proc = proc do |h, k|
- case k
- when String then sym = k.to_sym; h[sym] if h.key?(sym)
- when Symbol then str = k.to_s; h[str] if h.key?(str)
- end
- end
-
- #Initialization actions
- if args[:course_code].is_a? String and args[:course_code].isValidCourseCode?
- @course_code = args[:course_code]
-
- #Read JSON from the PCR API
- pcr = PCR::API.new()
- api_url = pcr.api_endpt + "coursehistories/" + self.course_code + "/?token=" + pcr.token
- json = JSON.parse(open(api_url).read)
-
- #Create array of Section objects, containing all Sections found in the API JSON for the Course
- @sections = []
- json["result"]["courses"].each do |c|
- @sections << Section.new(:aliases => c["aliases"], :id => c["id"], :name => c["name"], :path => c["path"], :semester => c["semester"], :hit_api => true)
- end
-
- #Set variables according to Course JSON data
- @id = json["result"]["id"]
- @name = json["result"]["name"]
- @path = json["result"]["path"]
-
- #Get reviews for the Course -- unfortunately this has to be a separate query
- api_url_reviews = pcr.api_endpt + "coursehistories/" + self.id.to_s + "/reviews?token=" + pcr.token
- json_reviews = JSON.parse(open(api_url_reviews).read)
- @reviews = json_reviews["result"]["values"]
-
- else
- raise CourseError, "Invalid course code specified. Use format [DEPT-###]."
- end
- end
-
- def average(metric)
- #Ensure that we know argument type
- if metric.is_a? Symbol
- metric = metric.to_s
- end
-
- if metric.is_a? String
- #Loop vars
- total = 0
- n = 0
-
- #For each section, check if ratings include metric arg -- if so, add metric rating to total and increment counting variable
- self.reviews.each do |review|
- ratings = review["ratings"]
- if ratings.include? metric
- total = total + review["ratings"][metric].to_f
- n = n + 1
- else
- raise CourseError, "No ratings found for \"#{metric}\" in #{self.name}."
- end
- end
-
- #Return average score as a float
- return (total/n)
-
- else
- raise CourseError, "Invalid metric format. Metric must be a string or symbol."
- end
- end
-
- def recent(metric)
- #Ensure that we know argument type
- if metric.is_a? Symbol
- metric = metric.to_s
- end
-
-
- if metric.is_a? String
- #Get the most recent section
- section = self.sections[-1]
-
- #Iterate through all the section reviews, and if the section review id matches the id of the most recent section, return that rating
- self.reviews.each do |review|
- if review["section"]["id"].to_s[0..4].to_i == section.id
- return review["ratings"][metric]
- end
- end
-
- raise CourseError, "No ratings found for #{metric} in #{section.semester}."
-
- else
- raise CourseError, "Invalid metric format. Metric must be a string or symbol."
- end
- end
- end
-
- #Section is an individual class under the umbrella of a general Course
- class Section
- attr_accessor :aliases, :id, :name, :path, :semester, :description, :comments, :ratings, :instructor
-
- def initialize(args)
- #Set indifferent access for args
- args.default_proc = proc do |h, k|
- case k
- when String then sym = k.to_sym; h[sym] if h.key?(sym)
- when Symbol then str = k.to_s; h[str] if h.key?(str)
- end
- end
-
- pcr = PCR::API.new()
- @aliases = args[:aliases] if args[:aliases].is_a? Array
- @id = args[:id] if args[:id].is_a? Integer
- @name = args[:name] if args[:name].is_a? String
- @path = args[:path] if args[:path].is_a? String
- @semester = args[:semester] if args[:semester].is_a? String
- @comments = ""
- @ratings = {}
- @instructor = {}
-
- if args[:hit_api]
- if args[:get_reviews]
- self.hit_api(:get_reviews => true)
- else
- self.hit_api(:get_reviews => false)
- end
- end
- end
-
- def hit_api(args)
- data = ["aliases", "name", "path", "semester", "description"]
- pcr = PCR::API.new()
- api_url = pcr.api_endpt + "courses/" + self.id.to_s + "?token=" + pcr.token
- json = JSON.parse(open(api_url).read)
-
- data.each do |d|
- case d
- when "aliases"
- self.instance_variable_set("@#{d}", json["result"]["aliases"])
- when "name"
- self.instance_variable_set("@#{d}", json["result"]["name"])
- when "path"
- self.instance_variable_set("@#{d}", json["result"]["path"])
- when "semester"
- self.instance_variable_set("@#{d}", json["result"]["semester"])
- when "description"
- self.instance_variable_set("@#{d}", json["result"]["description"])
- end
- end
-
- if args[:get_reviews]
- self.reviews()
- end
- end
-
- def reviews()
- pcr = PCR::API.new()
- api_url = pcr.api_endpt + "courses/" + self.id.to_s + "/reviews?token=" + pcr.token
- json = JSON.parse(open(api_url).read)
- @comments = []
- @ratings = []
- @instructors = []
- json["result"]["values"].each do |a|
- @comments << {a["instructor"]["id"] => a["comments"]}
- @ratings << {a["instructor"]["id"] => a["ratings"]}
- @instructors << a["instructor"]
- end
- # @comments = json["result"]["values"][0]["comments"]
- # @ratings = json["result"]["values"][0]["ratings"]
- # @instructor = json["result"]["values"][0]["instructor"]
-
- return {:comments => @comments, :ratings => @ratings}
- end
-
- def after(s)
- if s.is_a? Section
- self.semester.compareSemester(s.semester)
- elsif s.is_a? String
- self.semester.compareSemester(s)
- end
- end
- end
-
- #Instructor is a professor. Instructors are not tied to a course or section, but will have to be referenced from Sections.
- class Instructor
- attr_accessor :id, :name, :path, :sections, :reviews
-
- def initialize(id, args)
- #Set indifferent access for args
- args.default_proc = proc do |h, k|
- case k
- when String then sym = k.to_sym; h[sym] if h.key?(sym)
- when Symbol then str = k.to_s; h[str] if h.key?(str)
- end
- end
-
- #Assign args. ID is necessary because that's how we look up Instructors in the PCR API.
- if id.is_a? String
- @id = id
- else
- raise InstructorError("Invalid Instructor ID specified.")
- end
-
- @name = args[:name].downcase.titlecase if args[:name].is_a? String
- @path = args[:path] if args[:path].is_a? String
- @sections = args[:sections] if args[:sections].is_a? Hash
-
- #Hit PCR API to get missing info, if requested
- if args[:hit_api] == true
- self.getInfo
- self.getReviews
- end
- end
-
- #Hit the PCR API to get all missing info
- #Separate method in case we want to conduct it separately from a class init
- def getInfo
- pcr = PCR::API.new()
- api_url = pcr.api_endpt + "instructors/" + self.id + "?token=" + pcr.token
- json = JSON.parse(open(api_url).read)
-
- @name = json["result"]["name"].downcase.titlecase unless @name
- @path = json["result"]["path"] unless @path
- @sections = json["result"]["reviews"]["values"] unless @sections #Mislabeled reviews in PCR API
- end
-
- #Separate method for getting review data in case we don't want to make an extra API hit each init
- def getReviews
- if not self.reviews #make sure we don't already have reviews
- pcr = PCR::API.new()
- api_url = pcr.api_endpt + "instructors/" + self.id + "/reviews?token=" + pcr.token
- json = JSON.parse(open(api_url).read)
-
- @reviews = json["result"]["values"] #gets array
- end
- end
-
- #Get average value of a certain rating for Instructor
- def average(metric)
- #Ensure that we know argument type
- if metric.is_a? Symbol
- metric = metric.to_s
- end
-
- if metric.is_a? String
- #Loop vars
- total = 0
- n = 0
-
- #For each section, check if ratings include metric arg -- if so, add metric rating to total and increment counting variable
- self.getReviews
- self.reviews.each do |review|
- ratings = review["ratings"]
- if ratings.include? metric
- total = total + review["ratings"][metric].to_f
- n = n + 1
- else
- raise CourseError, "No ratings found for \"#{metric}\" for #{self.name}."
- end
- end
-
- #Return average score as a float
- return (total/n)
-
- else
- raise CourseError, "Invalid metric format. Metric must be a string or symbol."
- end
- end
-
- #Get most recent value of a certain rating for Instructor
- def recent(metric)
- #Ensure that we know argument type
- if metric.is_a? Symbol
- metric = metric.to_s
- end
-
- if metric.is_a? String
- #Iterate through reviews and create Section for each section reviewed, presented in an array
- sections = []
- section_ids = []
- self.getReviews
- self.reviews.each do |review|
- if section_ids.index(review["section"]["id"].to_i).nil?
- s = PCR::Section.new(:id => review["section"]["id"].to_i, :hit_api => false)
- sections << s
- section_ids << s.id
- end
- end
-
- #Get only most recent Section(s) in the array
- sections.reverse! #Newest first
- targets = []
- sections.each do |s|
- s.hit_api(:get_reviews => true)
- if sections.index(s) == 0
- targets << s
- elsif s.semester == sections[0].semester and s.id != sections[0].id
- targets << s
- else
- break
- end
- end
-
- #Calculate recent rating
- total = 0
- num = 0
- targets.each do |section|
- #Make sure we get the rating for the right Instructor
- section.ratings.each do |rating|
- if rating.key?(self.id)
- if rating[self.id][metric].nil?
- raise InstructorError, "No ratings found for #{metric} for #{self.name}."
- else
- total = total + rating[self.id][metric].to_f
- num += 1
- end
- end
- end
- end
-
- return total / num
-
- else
- raise CourseError, "Invalid metric format. Metric must be a string or symbol."
- end
- end
- end
-
-end #module
-
-#Some instance methods to handle instructor searching
-def downloadInstructors(instructors_db)
- pcr = PCR::API.new()
- api_url = pcr.api_endpt + "instructors/" + "?token=" + pcr.token
- #puts "Downloading instructors json..."
- json = JSON.parse(open(api_url).read)
-
- #Parse api data, writing to file
- begin
- File.open(instructors_db, 'w') do |f|
- instructor_hashes = json["result"]["values"]
- file_lines = []
- #puts "Constructing instructor file_lines"
- instructor_hashes.each do |instructor|
- n = instructor["name"].split(" ")
- file_lines << ["#{n[2]} #{n[1]} #{n[0]}",instructor["id"]] if n.length == 3 #if instructor has middle name
- file_lines << ["#{n[1]} #{n[0]}",instructor["id"]] if n.length == 2 #if instructor does not have middle name
- end
-
- #Sort lines alphabetically
- #puts "sorting file lines alphabetically..."
- file_lines.sort! { |a,b| a[0] <=> b[0] }
-
- #Write lines to csv file
- #puts "writing file lines to csv file..."
- file_lines.each { |line| f.write("#{line[0]},#{line[1]}\n") }
- end
- rescue IOError => e
- puts "Could not write to instructors file"
- rescue Errno::ENOENT => e
- puts "Could not open instructors file"
- end
+#PCR class handles token and api url, so both are easily changed
+class PCR
+ def initialize(token, api_endpt = "http://api.penncoursereview.com/v1/")
+ @@token = token
+ @@api_endpt = api_endpt
+ end
+
+ def course(course_code)
+ Course.new(course_code)
+ end
+
+ def section(*args)
+ Section.new(*args)
+ end
+
+ def instructor(id, *args)
+ Instructor.new(id, *args)
+ end
end
-def instructorSearch(args)
- #Set indifferent access for args
- args.default_proc = proc do |h, k|
- case k
- when String then sym = k.to_sym; h[sym] if h.key?(sym)
- when Symbol then str = k.to_s; h[str] if h.key?(str)
- end
- end
-
- #Set args
- first_name = args[:first_name]
- middle_initial = args[:middle_initial]
- last_name = args[:last_name]
-
- #Check if we've downloaded instructors in last week
- begin
- last_dl_time = Time.local(File.mtime("instructors.txt").tv_sec).tv_sec
- #puts last_dl_time
- rescue Errno::ENOENT => e
- downloadInstructors("instructors.txt") #instructors file doesn't exist, so download
- else
- current_time = Time.local(Time.now().tv_sec).tv_sec
- #puts current_time
- if current_time - last_dl_time <= 604800 #1 week in seconds
- downloadInstructors("instructors.txt")
- end
- end
-
- #Check if instructors file exists
- # begin
- # f = File.open("instructors.txt", "rb")
- # rescue Errno::ENOENT => e
- # downloadInstructors("instructors.txt")
- # end
-
- #Search for instructor name in instructors file and get corresponding ids, in an array
- #puts "searching instructors file..."
- results = []
- CSV.foreach("instructors.txt") do |line|
- results << {line[0] => line[1]} if line[0].include? last_name.upcase
- end
-
- return results
-end
+# Load classes
+Dir[File.dirname(__FILE__) + "/classes/*.rb"].each { |file| require file }
\ No newline at end of file