mitchellh-rubyuw 0.2.0
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 +1 -0
- data/README.markdown +49 -0
- data/Rakefile +13 -0
- data/VERSION +1 -0
- data/lib/myuw.rb +49 -0
- data/lib/myuw/session.rb +87 -0
- data/lib/myuw/sln.rb +89 -0
- data/rubyuw.gemspec +41 -0
- metadata +60 -0
data/.gitignore
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
pkg/*
|
data/README.markdown
ADDED
@@ -0,0 +1,49 @@
|
|
1
|
+
# RubyUW: Ruby Interface to MyUW
|
2
|
+
|
3
|
+
__RubyUW is NOT supported in any way by the University of Washington__
|
4
|
+
|
5
|
+
RubyUW provides a programmable interface to MyUW, University
|
6
|
+
of Washington's student web portal.
|
7
|
+
|
8
|
+
## Why?
|
9
|
+
|
10
|
+
Proof of Concept.
|
11
|
+
|
12
|
+
It's a fun project and it was really a proof of concept more than
|
13
|
+
anything. I don't plan on officially supporting this library or
|
14
|
+
promising updates in case any features break. Its a good example
|
15
|
+
for any Ruby developer on the uses of mechanize and the ability
|
16
|
+
to scrape content or simulate a human at a browser.
|
17
|
+
|
18
|
+
It is also a useful library to create MyUW automation tools
|
19
|
+
with. I do not support this.
|
20
|
+
|
21
|
+
## How does it work?
|
22
|
+
|
23
|
+
RubyUW functions by emulating a human in an actual browser. It hunts
|
24
|
+
down buttons to click, fields to fill in, etc. It is programmed
|
25
|
+
using the Ruby Mechanize library to achieve this level of human
|
26
|
+
simulation.
|
27
|
+
|
28
|
+
Of course this also means that even minor tweaks to the MyUW layout
|
29
|
+
could potentially "break" the RubyUW library.
|
30
|
+
|
31
|
+
## Installing
|
32
|
+
|
33
|
+
# Install the gem
|
34
|
+
sudo gem sources -a http://gems.github.com
|
35
|
+
sudo gem install mitchellh-rubyuw
|
36
|
+
|
37
|
+
## Using RubyUW
|
38
|
+
|
39
|
+
It's easy to get started with RubyUW. Officially RDoc documentation
|
40
|
+
is planned but is __not up yet__. Sorry!
|
41
|
+
|
42
|
+
The following is a quick and simple example:
|
43
|
+
|
44
|
+
require 'myuw'
|
45
|
+
myuw = MyUW.new
|
46
|
+
myuw.login("netid", "password") or raise("Login Failed!")
|
47
|
+
|
48
|
+
# Get SLN information
|
49
|
+
sln_info = myuw.sln(14153)
|
data/Rakefile
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
begin
|
2
|
+
require 'jeweler'
|
3
|
+
Jeweler::Tasks.new do |gemspec|
|
4
|
+
gemspec.name = "rubyuw"
|
5
|
+
gemspec.summary = "Library which provides a ruby interface to the University of Washington student portal."
|
6
|
+
gemspec.email = "mitchell.hashimoto@gmail.com"
|
7
|
+
gemspec.homepage = "http://github.com/mitchellh/rubyuw"
|
8
|
+
gemspec.description = "TODO"
|
9
|
+
gemspec.authors = ["Mitchell Hashimoto"]
|
10
|
+
end
|
11
|
+
rescue LoadError
|
12
|
+
puts "Jeweler not available. Install it with: sudo gem install technicalpickles-jeweler -s http://gems.github.com"
|
13
|
+
end
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.2.0
|
data/lib/myuw.rb
ADDED
@@ -0,0 +1,49 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'mechanize'
|
3
|
+
|
4
|
+
require 'lib/myuw/session'
|
5
|
+
require 'lib/myuw/sln'
|
6
|
+
|
7
|
+
class MyUW
|
8
|
+
extend Forwardable
|
9
|
+
|
10
|
+
##
|
11
|
+
# The version of the ruby MyUW automation class
|
12
|
+
VERSION = '0.1'
|
13
|
+
|
14
|
+
attr_accessor :browser
|
15
|
+
attr_accessor :session
|
16
|
+
|
17
|
+
def initialize
|
18
|
+
# Initialize the browser instance and mascarade
|
19
|
+
# around as Safari on Mac
|
20
|
+
@browser = WWW::Mechanize.new do |browser|
|
21
|
+
browser.user_agent_alias = 'Mac Safari'
|
22
|
+
browser.follow_meta_refresh = true
|
23
|
+
|
24
|
+
# Workaround to avoid frozen object error SSL pages
|
25
|
+
browser.keep_alive = false
|
26
|
+
end
|
27
|
+
|
28
|
+
# Initialize other members
|
29
|
+
@session = Session.new(self)
|
30
|
+
end
|
31
|
+
|
32
|
+
# Forward the session methods
|
33
|
+
def_delegator :session, :logged_in?
|
34
|
+
def_delegator :session, :logout
|
35
|
+
|
36
|
+
# Log into MyUW with a given username and password
|
37
|
+
def login(user, pass)
|
38
|
+
@session.email = user
|
39
|
+
@session.password = pass
|
40
|
+
@session.login
|
41
|
+
end
|
42
|
+
|
43
|
+
# Creates a new SLN object and returns it
|
44
|
+
def sln(sln_number)
|
45
|
+
sln_obj = SLNInfo.new(self)
|
46
|
+
sln_obj.sln = sln_number
|
47
|
+
return sln_obj
|
48
|
+
end
|
49
|
+
end
|
data/lib/myuw/session.rb
ADDED
@@ -0,0 +1,87 @@
|
|
1
|
+
class MyUW
|
2
|
+
# = Synopsis
|
3
|
+
# This class encapsulates a MyUW session. It handles
|
4
|
+
# the login/logout of the MyUW portal.
|
5
|
+
class Session
|
6
|
+
attr_accessor :myuw
|
7
|
+
attr_accessor :email
|
8
|
+
attr_accessor :password
|
9
|
+
|
10
|
+
def initialize(myuw=nil)
|
11
|
+
@myuw ||= myuw
|
12
|
+
|
13
|
+
@email = @password = nil
|
14
|
+
end
|
15
|
+
|
16
|
+
# Checks if a user is logged in. This method is actually
|
17
|
+
# pretty expensive in terms of time since it actually
|
18
|
+
# verifies by going to the MyUW homepage and seeing if the
|
19
|
+
# dashboard loads.
|
20
|
+
def logged_in?
|
21
|
+
home = @myuw.browser.get("http://myuw.washington.edu")
|
22
|
+
|
23
|
+
# Click the login button to hopefully get to the main MyUW
|
24
|
+
# page
|
25
|
+
entry_button = home.search("//input[@type='submit' and @value='Log in with your UW NetID']")
|
26
|
+
raise("Failed to find the log in button") if entry_button.empty?
|
27
|
+
|
28
|
+
relay_page = home.form_with(:name => 'f').submit()
|
29
|
+
welcome_msg = relay_page.search("span.greetingw")
|
30
|
+
return !welcome_msg.empty?
|
31
|
+
end
|
32
|
+
|
33
|
+
# Logs a user in using the given email and password
|
34
|
+
def login
|
35
|
+
# Make sure that email and password are filled out or this is
|
36
|
+
# useless
|
37
|
+
if @email.nil? || @password.nil?
|
38
|
+
raise("Email and password weren't specified for MyUW session")
|
39
|
+
end
|
40
|
+
|
41
|
+
# Log out first
|
42
|
+
logout
|
43
|
+
|
44
|
+
# Go to the MyUW page and get to the login form
|
45
|
+
login_page = get_login_page
|
46
|
+
login_form = login_page.form_with(:name => 'query')
|
47
|
+
raise("Login form was not found on the MyUW page") if login_form.nil?
|
48
|
+
login_form.user = @email
|
49
|
+
login_form.pass = @password
|
50
|
+
relay_page = login_form.submit()
|
51
|
+
|
52
|
+
# Check if the login failed
|
53
|
+
unless relay_page.form_with(:name => 'query').nil?
|
54
|
+
return false
|
55
|
+
end
|
56
|
+
|
57
|
+
# Follow the relay
|
58
|
+
follow_relay_page(relay_page)
|
59
|
+
true
|
60
|
+
end
|
61
|
+
|
62
|
+
# Log out of the MyUW site
|
63
|
+
def logout
|
64
|
+
@myuw.browser.cookie_jar.clear!
|
65
|
+
end
|
66
|
+
|
67
|
+
private
|
68
|
+
|
69
|
+
def get_login_page
|
70
|
+
home = @myuw.browser.get("http://myuw.washington.edu")
|
71
|
+
|
72
|
+
# Click the login button to get to the forum
|
73
|
+
entry_button = home.search("//input[@type='submit' and @value='Log in with your UW NetID']")
|
74
|
+
raise("Failed to find the log in button") if entry_button.empty?
|
75
|
+
|
76
|
+
entry_form = home.form_with(:name => 'f')
|
77
|
+
relay_page = entry_form.submit()
|
78
|
+
|
79
|
+
return follow_relay_page(relay_page)
|
80
|
+
end
|
81
|
+
|
82
|
+
def follow_relay_page(relay_page)
|
83
|
+
relay_form = relay_page.form_with(:name => 'relay')
|
84
|
+
relay_form.submit()
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
data/lib/myuw/sln.rb
ADDED
@@ -0,0 +1,89 @@
|
|
1
|
+
class MyUW
|
2
|
+
# = Synopsis
|
3
|
+
# Gets information regarding a specific SLN, returning
|
4
|
+
# information in a SLN object.
|
5
|
+
class SLNInfo
|
6
|
+
attr_accessor :term
|
7
|
+
attr_reader :sln
|
8
|
+
attr_accessor :myuw
|
9
|
+
attr_reader :data
|
10
|
+
|
11
|
+
def initialize(myuw)
|
12
|
+
@myuw = myuw
|
13
|
+
@term = "AUT+2009"
|
14
|
+
@data = nil
|
15
|
+
@sln = nil
|
16
|
+
end
|
17
|
+
|
18
|
+
# Custom setter for SLN to reset data
|
19
|
+
def sln=(value)
|
20
|
+
@sln = value
|
21
|
+
@data = nil
|
22
|
+
end
|
23
|
+
|
24
|
+
# Fetches the information for the given SLN.
|
25
|
+
def fetch_data
|
26
|
+
raise("SLN not set.") if @sln.nil?
|
27
|
+
raise("User must be logged in to fetch SLN data") unless @myuw.logged_in?
|
28
|
+
|
29
|
+
page = @myuw.browser.get("https://sdb.admin.washington.edu/timeschd/uwnetid/sln.asp?QTRYR=#{@term}&SLN=#{@sln}")
|
30
|
+
if page.uri == "http://www.washington.edu/students/timeschd/badrequest.html" then
|
31
|
+
raise("Attempted to fetch SLN data too soon.")
|
32
|
+
end
|
33
|
+
|
34
|
+
# Get the SLN information. I use a pretty sneaky xpath query
|
35
|
+
# here which is VERY LIKELY to break given any changes to the
|
36
|
+
# design of UW time schedules
|
37
|
+
info_nodes = page.search("//table[@border=1 and @cellpadding=3]//tr[@rowspan=1]//td")
|
38
|
+
|
39
|
+
data_order = [nil, :course, :section, :type, :credits, :title]
|
40
|
+
info_nodes.each_with_index do |node, i|
|
41
|
+
if i < data_order.length then
|
42
|
+
data_key = data_order[i]
|
43
|
+
|
44
|
+
unless data_key.nil?
|
45
|
+
@data ||= {}
|
46
|
+
@data[data_key] = node.inner_text.strip
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
# Get the enrollment information. Uses a pretty complicated
|
52
|
+
# xpath query once again which is likely to break given any
|
53
|
+
# changes to the design of the UW time schedules.
|
54
|
+
info_nodes = page.search("//table[@border=1 and @cellpadding=3]//tr[count(td)=5]//td")
|
55
|
+
|
56
|
+
data_order = [:current_enrollment, :limit_enrollment, :room_capacity, :space_available, :status]
|
57
|
+
info_nodes.each_with_index do |node, i|
|
58
|
+
if i < data_order.length then
|
59
|
+
data_key = data_order[i]
|
60
|
+
|
61
|
+
unless data_key.nil?
|
62
|
+
@data ||= {}
|
63
|
+
@data[data_key] = node.inner_text.strip
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
# And finally getting the notes of an SLN, accompanied by
|
69
|
+
# by far the ugliest xpath query yet.
|
70
|
+
info_nodes = page.search("//table[@border=1 and @cellpadding=3]//tr[count(td)=1]//preceding-sibling::tr[count(th)=1]//following-sibling::tr//td")
|
71
|
+
@data ||= {}
|
72
|
+
@data[:notes] = info_nodes[0].inner_text.strip
|
73
|
+
end
|
74
|
+
|
75
|
+
# The methods to extract various info out of the SLN. While
|
76
|
+
# I tend to try to avoid metaprogramming, this case seemed
|
77
|
+
# simple enough. Nothing tricky happening here!
|
78
|
+
[:course, :section, :type, :credits, :title, :current_enrollment,
|
79
|
+
:limit_enrollment, :room_capacity, :space_available, :status,
|
80
|
+
:notes].each do |info|
|
81
|
+
eval(<<-eomethod)
|
82
|
+
def #{info}
|
83
|
+
fetch_data if @data.nil?
|
84
|
+
@data[:#{info}]
|
85
|
+
end
|
86
|
+
eomethod
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
data/rubyuw.gemspec
ADDED
@@ -0,0 +1,41 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
|
3
|
+
Gem::Specification.new do |s|
|
4
|
+
s.name = %q{rubyuw}
|
5
|
+
s.version = "0.2.0"
|
6
|
+
|
7
|
+
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
8
|
+
s.authors = ["Mitchell Hashimoto"]
|
9
|
+
s.date = %q{2009-06-21}
|
10
|
+
s.description = %q{TODO}
|
11
|
+
s.email = %q{mitchell.hashimoto@gmail.com}
|
12
|
+
s.extra_rdoc_files = [
|
13
|
+
"README.markdown"
|
14
|
+
]
|
15
|
+
s.files = [
|
16
|
+
".gitignore",
|
17
|
+
"README.markdown",
|
18
|
+
"Rakefile",
|
19
|
+
"VERSION",
|
20
|
+
"lib/myuw.rb",
|
21
|
+
"lib/myuw/session.rb",
|
22
|
+
"lib/myuw/sln.rb",
|
23
|
+
"rubyuw.gemspec"
|
24
|
+
]
|
25
|
+
s.has_rdoc = true
|
26
|
+
s.homepage = %q{http://github.com/mitchellh/rubyuw}
|
27
|
+
s.rdoc_options = ["--charset=UTF-8"]
|
28
|
+
s.require_paths = ["lib"]
|
29
|
+
s.rubygems_version = %q{1.3.1}
|
30
|
+
s.summary = %q{Library which provides a ruby interface to the University of Washington student portal.}
|
31
|
+
|
32
|
+
if s.respond_to? :specification_version then
|
33
|
+
current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
|
34
|
+
s.specification_version = 2
|
35
|
+
|
36
|
+
if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
|
37
|
+
else
|
38
|
+
end
|
39
|
+
else
|
40
|
+
end
|
41
|
+
end
|
metadata
ADDED
@@ -0,0 +1,60 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: mitchellh-rubyuw
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.2.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Mitchell Hashimoto
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2009-06-21 00:00:00 -07:00
|
13
|
+
default_executable:
|
14
|
+
dependencies: []
|
15
|
+
|
16
|
+
description: TODO
|
17
|
+
email: mitchell.hashimoto@gmail.com
|
18
|
+
executables: []
|
19
|
+
|
20
|
+
extensions: []
|
21
|
+
|
22
|
+
extra_rdoc_files:
|
23
|
+
- README.markdown
|
24
|
+
files:
|
25
|
+
- .gitignore
|
26
|
+
- README.markdown
|
27
|
+
- Rakefile
|
28
|
+
- VERSION
|
29
|
+
- lib/myuw.rb
|
30
|
+
- lib/myuw/session.rb
|
31
|
+
- lib/myuw/sln.rb
|
32
|
+
- rubyuw.gemspec
|
33
|
+
has_rdoc: true
|
34
|
+
homepage: http://github.com/mitchellh/rubyuw
|
35
|
+
post_install_message:
|
36
|
+
rdoc_options:
|
37
|
+
- --charset=UTF-8
|
38
|
+
require_paths:
|
39
|
+
- lib
|
40
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
41
|
+
requirements:
|
42
|
+
- - ">="
|
43
|
+
- !ruby/object:Gem::Version
|
44
|
+
version: "0"
|
45
|
+
version:
|
46
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
47
|
+
requirements:
|
48
|
+
- - ">="
|
49
|
+
- !ruby/object:Gem::Version
|
50
|
+
version: "0"
|
51
|
+
version:
|
52
|
+
requirements: []
|
53
|
+
|
54
|
+
rubyforge_project:
|
55
|
+
rubygems_version: 1.2.0
|
56
|
+
signing_key:
|
57
|
+
specification_version: 2
|
58
|
+
summary: Library which provides a ruby interface to the University of Washington student portal.
|
59
|
+
test_files: []
|
60
|
+
|