uw_learn 1.0.0.pre → 1.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.
- 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: []
|