uw_learn 1.0.0.pre → 1.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/lib/uw_learn.rb +89 -9
- data/lib/uw_learn/course.rb +40 -0
- data/lib/uw_learn/grade.rb +116 -0
- metadata +155 -8
data/lib/uw_learn.rb
CHANGED
@@ -1,15 +1,28 @@
|
|
1
1
|
require 'mechanize'
|
2
2
|
require 'hpricot'
|
3
|
-
require 'awesome_print'
|
4
3
|
require 'open-uri'
|
5
4
|
require 'httparty'
|
6
5
|
require 'rainbow'
|
7
6
|
|
8
|
-
require '
|
9
|
-
require '
|
7
|
+
require 'uw_learn/grade'
|
8
|
+
require 'uw_learn/course'
|
10
9
|
|
10
|
+
##
|
11
|
+
# The UW Learn gem is a tiny web crawler for University of Waterloo students.
|
12
|
+
# It grabs their grades, and prints them out in the terminal.
|
13
|
+
#
|
14
|
+
# @author Gaurav Mali <hello@gauravmali.com>
|
15
|
+
# @version 1.0.1
|
16
|
+
#
|
11
17
|
class Uwlearn
|
12
|
-
|
18
|
+
|
19
|
+
##
|
20
|
+
# Creates a new instance of UW Learn account.
|
21
|
+
# Proceeds to login and scrape grades.
|
22
|
+
#
|
23
|
+
# @param [String] The student username
|
24
|
+
# @param [String] The student password
|
25
|
+
#
|
13
26
|
def initialize(login, password)
|
14
27
|
@user_login = login
|
15
28
|
@user_passw = password
|
@@ -24,20 +37,38 @@ class Uwlearn
|
|
24
37
|
start
|
25
38
|
end
|
26
39
|
|
40
|
+
##
|
41
|
+
# @!method login
|
42
|
+
# The username that the account was logged in with
|
43
|
+
# @return [String] The username
|
44
|
+
#
|
27
45
|
def login
|
28
46
|
@user_login
|
29
47
|
end
|
30
48
|
|
49
|
+
##
|
50
|
+
# @!method print_courses
|
51
|
+
# Prints out the names of all the courses
|
52
|
+
#
|
31
53
|
def print_courses
|
32
54
|
@learn_courses.each do |course|
|
33
55
|
puts course.name.foreground(:yellow)
|
34
56
|
end
|
35
57
|
end
|
36
58
|
|
59
|
+
##
|
60
|
+
# @!method courses
|
61
|
+
# The array of the user's courses in string format
|
62
|
+
# @return [Array] The user's courses
|
63
|
+
#
|
37
64
|
def courses
|
38
65
|
@learn_courses
|
39
66
|
end
|
40
67
|
|
68
|
+
##
|
69
|
+
# @!method print_grades
|
70
|
+
# Prints out every course's name and the grades registered with it
|
71
|
+
#
|
41
72
|
def print_grades
|
42
73
|
@learn_grades.each do |grade|
|
43
74
|
puts grade.which_course?.foreground(:yellow) + ": ".foreground(:yellow)
|
@@ -45,43 +76,82 @@ class Uwlearn
|
|
45
76
|
end
|
46
77
|
end
|
47
78
|
|
79
|
+
##
|
80
|
+
# @!method grades
|
81
|
+
# The array of user's grades in string format
|
82
|
+
# @return [Array] The user's grades
|
83
|
+
#
|
48
84
|
def grades
|
49
85
|
@learn_grades
|
50
86
|
end
|
51
87
|
|
52
88
|
private
|
53
89
|
|
90
|
+
##
|
91
|
+
# learn_url
|
92
|
+
# Holding on to the learn URL for easy change. You may try simply changing this URL to a different University's URL.
|
93
|
+
# But remember, this is a hacky scrape job. Don't be surprised if things break.
|
94
|
+
#
|
54
95
|
def learn_url
|
55
96
|
"http://learn.uwaterloo.ca"
|
56
97
|
end
|
57
98
|
|
99
|
+
##
|
100
|
+
# course_url
|
101
|
+
# Just in case if D2L decides to change their markup, you know where to look for (hopefully) a quick fix.
|
102
|
+
#
|
58
103
|
def course_url
|
59
104
|
"/d2l/lp/ouHome/home.d2l?ou="
|
60
105
|
end
|
61
106
|
|
107
|
+
##
|
108
|
+
# course_html
|
109
|
+
# Just in case if D2L decides to change their markup, you know where to look for (hopefully) a quick fix.
|
110
|
+
#
|
62
111
|
def course_html
|
63
|
-
"//ul[@id='
|
112
|
+
"//ul[@id='z_w']//div[@class='dco_c']//a"
|
113
|
+
end
|
114
|
+
|
115
|
+
##
|
116
|
+
# error_html
|
117
|
+
# Just in case if D2L decides to change their markup, you know where to look for (hopefully) a quick fix.
|
118
|
+
#
|
119
|
+
def error_html
|
120
|
+
"//div[@class='error']"
|
64
121
|
end
|
65
122
|
|
123
|
+
##
|
124
|
+
# start
|
125
|
+
# The starting point of crawling. Calls the authentication and scraping methods.
|
126
|
+
#
|
66
127
|
def start
|
128
|
+
# Very important object. Don't lose or overwrite it. Required for scraping.
|
67
129
|
agent = Mechanize.new
|
68
130
|
|
69
131
|
begin
|
132
|
+
# Authenticates in this method call.
|
70
133
|
links = authenticate agent
|
134
|
+
# Scrapes in this method call.
|
71
135
|
acquire links, agent
|
136
|
+
|
72
137
|
puts "Grades acquired.".foreground(:cyan)
|
73
138
|
rescue Exception => e
|
74
|
-
puts e.message
|
75
|
-
#puts e.backtrace.inspect
|
139
|
+
puts e.message
|
76
140
|
rescue Mechanize::ResponseCodeError, Net::HTTPNotFound
|
77
141
|
puts "Looks like the website has changed. Contact the developer to fix this issue.".foreground(:red)
|
78
142
|
end
|
79
143
|
|
80
144
|
end
|
81
145
|
|
146
|
+
##
|
147
|
+
# authenticate
|
148
|
+
# Logs in using the user credentials.
|
149
|
+
# Returns an array of anchor(<a href=""></a>) elements of course links.
|
150
|
+
#
|
82
151
|
def authenticate(agent)
|
83
152
|
puts "Authenticating ".foreground(:cyan) + @user_login.foreground(:cyan) + "...".foreground(:cyan)
|
84
153
|
|
154
|
+
# Login form submission
|
85
155
|
page = agent.get learn_url
|
86
156
|
form = page.forms.first
|
87
157
|
form.username = @user_login
|
@@ -89,13 +159,23 @@ class Uwlearn
|
|
89
159
|
page = agent.submit form
|
90
160
|
|
91
161
|
course_links = page.search course_html
|
92
|
-
|
162
|
+
login_error = page.search error_html
|
163
|
+
|
164
|
+
if login_error.empty? and course_links.empty?
|
165
|
+
raise "D2l has changed. Please contact the developer.".foreground(:red)
|
166
|
+
elsif !login_error.empty?
|
93
167
|
raise "Couldn't authenticate. Please try again.".foreground(:red)
|
94
168
|
end
|
95
169
|
|
96
170
|
course_links
|
97
171
|
end
|
98
172
|
|
173
|
+
##
|
174
|
+
# acquire
|
175
|
+
# Extracts the course name and code for further scraping of grades.
|
176
|
+
# The scraping occurs in the initialization of the grade objects.
|
177
|
+
# Stores the grades and courses in two different arrays: @learn_courses and @learn_grades.
|
178
|
+
#
|
99
179
|
def acquire(links, agent)
|
100
180
|
puts "Acquiring data...".foreground(:cyan)
|
101
181
|
|
@@ -115,4 +195,4 @@ class Uwlearn
|
|
115
195
|
end
|
116
196
|
end
|
117
197
|
|
118
|
-
end
|
198
|
+
end # Uwlearn
|
@@ -0,0 +1,40 @@
|
|
1
|
+
##
|
2
|
+
# The course object represents a typical course a student would register for.
|
3
|
+
#
|
4
|
+
# @todo Add more featuers such as course news and dates
|
5
|
+
#
|
6
|
+
# @author Gaurav Mali <hello@gauravmali.com>
|
7
|
+
# @version 1.0.1
|
8
|
+
#
|
9
|
+
|
10
|
+
class Course
|
11
|
+
##
|
12
|
+
# @!attribute [r] name
|
13
|
+
# @return [String] the course name
|
14
|
+
attr_reader :name
|
15
|
+
|
16
|
+
##
|
17
|
+
# @!attribute [r] code
|
18
|
+
# @return [String] the course code
|
19
|
+
attr_reader :code
|
20
|
+
|
21
|
+
##
|
22
|
+
# @!attribute [r] grade
|
23
|
+
# @return [Object] the grades of the particular course
|
24
|
+
attr_reader :grade
|
25
|
+
|
26
|
+
##
|
27
|
+
# Creates a new instance of a course object.
|
28
|
+
# Simply holds on to string values and the grade object.
|
29
|
+
#
|
30
|
+
# @param [String] The course name
|
31
|
+
# @param [String] The course code
|
32
|
+
# @param [Object] The course grades
|
33
|
+
#
|
34
|
+
def initialize(name, code, grade)
|
35
|
+
@name = name
|
36
|
+
@code = code
|
37
|
+
@grade = grade
|
38
|
+
end
|
39
|
+
|
40
|
+
end # Course
|
@@ -0,0 +1,116 @@
|
|
1
|
+
require 'mechanize'
|
2
|
+
require 'hpricot'
|
3
|
+
require 'open-uri'
|
4
|
+
|
5
|
+
##
|
6
|
+
# The grade object scrapes and holds on the grades of a particular course
|
7
|
+
#
|
8
|
+
# @todo Store the grades in a hash with key as a string and value as a number of some sort
|
9
|
+
#
|
10
|
+
# @author Gaurav Mali <hello@gauravmali.com>
|
11
|
+
# @version 1.0.1
|
12
|
+
#
|
13
|
+
class Grade
|
14
|
+
|
15
|
+
##
|
16
|
+
# Creates a new instance of a course grade.
|
17
|
+
# Proceeds to scrape grades and store it in @grades.
|
18
|
+
#
|
19
|
+
# @param [String] The course name
|
20
|
+
# @param [String] The course code
|
21
|
+
# @param [Object] The Mechanize object for scraping
|
22
|
+
#
|
23
|
+
def initialize(name, code, agent)
|
24
|
+
@course_name = name
|
25
|
+
@course_code = code
|
26
|
+
@grades = Array.new
|
27
|
+
|
28
|
+
get_grades agent
|
29
|
+
end
|
30
|
+
|
31
|
+
##
|
32
|
+
# @!method which_course?
|
33
|
+
# A way to find out which course a grade belongs to.
|
34
|
+
# @return [String] The course name
|
35
|
+
#
|
36
|
+
def which_course?
|
37
|
+
@course_name
|
38
|
+
end
|
39
|
+
|
40
|
+
##
|
41
|
+
# @!method summary
|
42
|
+
# Returns a summary of grades for this particular course.
|
43
|
+
# If no grades were uploaded, user is notified so.
|
44
|
+
#
|
45
|
+
# @return [Array] The grades in string format.
|
46
|
+
#
|
47
|
+
def summary
|
48
|
+
if @grades.empty?
|
49
|
+
("Sorry the instructor for " + @course_name + " has not uploaded any marks yet").foreground(:red)
|
50
|
+
else
|
51
|
+
@grades
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
private
|
56
|
+
|
57
|
+
##
|
58
|
+
# grades_url
|
59
|
+
# Just in case if D2L decides to change their markup, you know where to look for (hopefully) a quick fix.
|
60
|
+
#
|
61
|
+
def grades_url
|
62
|
+
"https://learn.uwaterloo.ca/d2l/lms/grades/my_grades/main.d2l?ou=" + @course_code
|
63
|
+
end
|
64
|
+
|
65
|
+
##
|
66
|
+
# grades?
|
67
|
+
# Are there any grades for this course?
|
68
|
+
# Returns true(yes) or false(no)
|
69
|
+
#
|
70
|
+
def grades?(report)
|
71
|
+
report.xpath("//td[@class='d_gempty']").to_s == ""
|
72
|
+
end
|
73
|
+
|
74
|
+
##
|
75
|
+
# has_mark?
|
76
|
+
# Are there any marks in this table of grades?
|
77
|
+
# We don't want rows with no data in them.
|
78
|
+
#
|
79
|
+
# Returns true(yes) or false(no)
|
80
|
+
#
|
81
|
+
def has_mark?(row)
|
82
|
+
# Why didn't !(row/"label").blank? work?
|
83
|
+
(row/"label").to_s != ""
|
84
|
+
end
|
85
|
+
|
86
|
+
##
|
87
|
+
# get_grades
|
88
|
+
# More scraping.
|
89
|
+
#
|
90
|
+
def get_grades(agent)
|
91
|
+
page = agent.get grades_url
|
92
|
+
report = page.parser
|
93
|
+
|
94
|
+
# Search for all rows that have both 'strong' and 'label' elements
|
95
|
+
if grades? report
|
96
|
+
rows = report.xpath("//table[@id='z_c']/tr")
|
97
|
+
|
98
|
+
if rows.empty?
|
99
|
+
raise "ERROR: Failed parsing at the rows. Please contact the developer.".foreground(:red)
|
100
|
+
end
|
101
|
+
|
102
|
+
rows.each do |row|
|
103
|
+
if has_mark? row
|
104
|
+
mark = (row/"label").last.inner_html.to_s
|
105
|
+
category = (row/"strong").inner_html.to_s
|
106
|
+
|
107
|
+
# Storing it as a string for now. Future releases will be different.
|
108
|
+
@grades << (category + " => " + mark)
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
end
|
113
|
+
|
114
|
+
end
|
115
|
+
|
116
|
+
end # Grade
|
metadata
CHANGED
@@ -1,17 +1,161 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: uw_learn
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.0.
|
5
|
-
prerelease:
|
4
|
+
version: 1.0.1
|
5
|
+
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
8
8
|
- Gaurav Mali
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2012-
|
13
|
-
dependencies:
|
14
|
-
|
12
|
+
date: 2012-09-14 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: mechanize
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '0'
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ! '>='
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '0'
|
30
|
+
- !ruby/object:Gem::Dependency
|
31
|
+
name: hpricot
|
32
|
+
requirement: !ruby/object:Gem::Requirement
|
33
|
+
none: false
|
34
|
+
requirements:
|
35
|
+
- - ! '>='
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: '0'
|
38
|
+
type: :runtime
|
39
|
+
prerelease: false
|
40
|
+
version_requirements: !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ! '>='
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: '0'
|
46
|
+
- !ruby/object:Gem::Dependency
|
47
|
+
name: httparty
|
48
|
+
requirement: !ruby/object:Gem::Requirement
|
49
|
+
none: false
|
50
|
+
requirements:
|
51
|
+
- - ! '>='
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '0'
|
54
|
+
type: :runtime
|
55
|
+
prerelease: false
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
57
|
+
none: false
|
58
|
+
requirements:
|
59
|
+
- - ! '>='
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
- !ruby/object:Gem::Dependency
|
63
|
+
name: rainbow
|
64
|
+
requirement: !ruby/object:Gem::Requirement
|
65
|
+
none: false
|
66
|
+
requirements:
|
67
|
+
- - ! '>='
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
version: '0'
|
70
|
+
type: :runtime
|
71
|
+
prerelease: false
|
72
|
+
version_requirements: !ruby/object:Gem::Requirement
|
73
|
+
none: false
|
74
|
+
requirements:
|
75
|
+
- - ! '>='
|
76
|
+
- !ruby/object:Gem::Version
|
77
|
+
version: '0'
|
78
|
+
- !ruby/object:Gem::Dependency
|
79
|
+
name: webmock
|
80
|
+
requirement: !ruby/object:Gem::Requirement
|
81
|
+
none: false
|
82
|
+
requirements:
|
83
|
+
- - ! '>='
|
84
|
+
- !ruby/object:Gem::Version
|
85
|
+
version: '0'
|
86
|
+
type: :development
|
87
|
+
prerelease: false
|
88
|
+
version_requirements: !ruby/object:Gem::Requirement
|
89
|
+
none: false
|
90
|
+
requirements:
|
91
|
+
- - ! '>='
|
92
|
+
- !ruby/object:Gem::Version
|
93
|
+
version: '0'
|
94
|
+
- !ruby/object:Gem::Dependency
|
95
|
+
name: minitest
|
96
|
+
requirement: !ruby/object:Gem::Requirement
|
97
|
+
none: false
|
98
|
+
requirements:
|
99
|
+
- - ! '>='
|
100
|
+
- !ruby/object:Gem::Version
|
101
|
+
version: '0'
|
102
|
+
type: :development
|
103
|
+
prerelease: false
|
104
|
+
version_requirements: !ruby/object:Gem::Requirement
|
105
|
+
none: false
|
106
|
+
requirements:
|
107
|
+
- - ! '>='
|
108
|
+
- !ruby/object:Gem::Version
|
109
|
+
version: '0'
|
110
|
+
- !ruby/object:Gem::Dependency
|
111
|
+
name: vcr
|
112
|
+
requirement: !ruby/object:Gem::Requirement
|
113
|
+
none: false
|
114
|
+
requirements:
|
115
|
+
- - ! '>='
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: '0'
|
118
|
+
type: :development
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
none: false
|
122
|
+
requirements:
|
123
|
+
- - ! '>='
|
124
|
+
- !ruby/object:Gem::Version
|
125
|
+
version: '0'
|
126
|
+
- !ruby/object:Gem::Dependency
|
127
|
+
name: turn
|
128
|
+
requirement: !ruby/object:Gem::Requirement
|
129
|
+
none: false
|
130
|
+
requirements:
|
131
|
+
- - ! '>='
|
132
|
+
- !ruby/object:Gem::Version
|
133
|
+
version: '0'
|
134
|
+
type: :development
|
135
|
+
prerelease: false
|
136
|
+
version_requirements: !ruby/object:Gem::Requirement
|
137
|
+
none: false
|
138
|
+
requirements:
|
139
|
+
- - ! '>='
|
140
|
+
- !ruby/object:Gem::Version
|
141
|
+
version: '0'
|
142
|
+
- !ruby/object:Gem::Dependency
|
143
|
+
name: rake
|
144
|
+
requirement: !ruby/object:Gem::Requirement
|
145
|
+
none: false
|
146
|
+
requirements:
|
147
|
+
- - ! '>='
|
148
|
+
- !ruby/object:Gem::Version
|
149
|
+
version: '0'
|
150
|
+
type: :development
|
151
|
+
prerelease: false
|
152
|
+
version_requirements: !ruby/object:Gem::Requirement
|
153
|
+
none: false
|
154
|
+
requirements:
|
155
|
+
- - ! '>='
|
156
|
+
- !ruby/object:Gem::Version
|
157
|
+
version: '0'
|
158
|
+
description: Displays student grades from D2l in the terminal.
|
15
159
|
email: hello@gauravmali.com
|
16
160
|
executables:
|
17
161
|
- uw_learn
|
@@ -19,6 +163,8 @@ extensions: []
|
|
19
163
|
extra_rdoc_files: []
|
20
164
|
files:
|
21
165
|
- lib/uw_learn.rb
|
166
|
+
- lib/uw_learn/grade.rb
|
167
|
+
- lib/uw_learn/course.rb
|
22
168
|
- bin/uw_learn
|
23
169
|
homepage: https://github.com/GMali/uw_learn
|
24
170
|
licenses: []
|
@@ -26,6 +172,7 @@ post_install_message:
|
|
26
172
|
rdoc_options: []
|
27
173
|
require_paths:
|
28
174
|
- lib
|
175
|
+
- lib
|
29
176
|
required_ruby_version: !ruby/object:Gem::Requirement
|
30
177
|
none: false
|
31
178
|
requirements:
|
@@ -35,13 +182,13 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
35
182
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
36
183
|
none: false
|
37
184
|
requirements:
|
38
|
-
- - ! '
|
185
|
+
- - ! '>='
|
39
186
|
- !ruby/object:Gem::Version
|
40
|
-
version:
|
187
|
+
version: '0'
|
41
188
|
requirements: []
|
42
189
|
rubyforge_project:
|
43
190
|
rubygems_version: 1.8.24
|
44
191
|
signing_key:
|
45
192
|
specification_version: 3
|
46
|
-
summary: Tiny web crawler for University of Waterloo students
|
193
|
+
summary: Tiny web crawler for University of Waterloo students.
|
47
194
|
test_files: []
|