ubcbooker 0.1.0 → 0.2.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +2 -0
- data/README.md +10 -7
- data/bin/console +2 -8
- data/bin/ubcbooker +6 -0
- data/lib/ubcbooker/account.rb +31 -0
- data/lib/ubcbooker/cli.rb +156 -0
- data/lib/ubcbooker/cli_validator.rb +50 -0
- data/lib/ubcbooker/color.rb +16 -0
- data/lib/ubcbooker/config.rb +39 -0
- data/lib/ubcbooker/error.rb +70 -0
- data/lib/ubcbooker/sample.yml +3 -0
- data/lib/ubcbooker/scrapers/base_scraper.rb +43 -0
- data/lib/ubcbooker/scrapers/cs.rb +175 -0
- data/lib/ubcbooker/scrapers/scraper.rb +2 -0
- data/lib/ubcbooker/version.rb +1 -1
- data/lib/ubcbooker.rb +20 -46
- data/ubcbooker.gemspec +7 -4
- metadata +82 -15
- data/Gemfile.lock +0 -86
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: feb2fd80cc7567a4cfd673c7245ac5bd4251fa8b
|
4
|
+
data.tar.gz: f1f96fc070d9e8f66d955f103fa3c3c950597a74
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 702723bae1f951c48ceddbf121100b9858fc25e32aa48d00b9fd0cd26c5029cc34fdb2b6173e4bcee2077dc37114310fa4f3950cb6a35cd214822ce480107e6e
|
7
|
+
data.tar.gz: 9dd869bad331752435f49821fc93d1a345dcefbf99ff3a77f7a7ccafea3b25350f074404e7a0929eb4007e9aab49f3f59d22abc37270a5dd4a8b8580d7df0046
|
data/.gitignore
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
.DS_Store
|
2
2
|
*.gem
|
3
3
|
*.rbc
|
4
|
+
lib/ubcbooker/config.yml
|
4
5
|
/.config
|
5
6
|
/coverage/
|
6
7
|
/InstalledFiles
|
@@ -25,6 +26,7 @@
|
|
25
26
|
Gemfile.lock
|
26
27
|
.ruby-version
|
27
28
|
.ruby-gemset
|
29
|
+
.rspec_status
|
28
30
|
|
29
31
|
# unless supporting rvm < 1.11.0 or doing something fancy, ignore this:
|
30
32
|
.rvmrc
|
data/README.md
CHANGED
@@ -4,7 +4,11 @@ CLI tool to book project rooms in UBC
|
|
4
4
|
|
5
5
|
## Installation
|
6
6
|
|
7
|
-
|
7
|
+
Install it as:
|
8
|
+
|
9
|
+
$ gem install ubcbooker
|
10
|
+
|
11
|
+
For use in your application, add this line to your Gemfile:
|
8
12
|
|
9
13
|
```ruby
|
10
14
|
gem 'ubcbooker'
|
@@ -14,21 +18,20 @@ And then execute:
|
|
14
18
|
|
15
19
|
$ bundle
|
16
20
|
|
17
|
-
Or install it yourself as:
|
18
|
-
|
19
|
-
$ gem install ubcbooker
|
20
|
-
|
21
21
|
## Usage
|
22
22
|
|
23
23
|
```
|
24
|
-
# Call help
|
25
|
-
$ ubcbooker -h
|
26
24
|
# Book Computer Science project rooms
|
27
25
|
$ ubcbooker cs
|
28
26
|
# Book Sauder project rooms
|
29
27
|
$ ubcbooker sauder
|
30
28
|
```
|
31
29
|
|
30
|
+
```
|
31
|
+
# Call help
|
32
|
+
$ ubcbooker -h
|
33
|
+
```
|
34
|
+
|
32
35
|
## Development
|
33
36
|
|
34
37
|
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment. To install this gem onto your local machine, run `bundle exec rake install`.
|
data/bin/console
CHANGED
@@ -2,13 +2,7 @@
|
|
2
2
|
|
3
3
|
require "bundler/setup"
|
4
4
|
require "ubcbooker"
|
5
|
+
require "pry"
|
5
6
|
|
6
|
-
|
7
|
-
# with your gem easier. You can also use a different console, if you like.
|
7
|
+
Pry.start
|
8
8
|
|
9
|
-
# (If you use this, don't forget to add pry to your Gemfile!)
|
10
|
-
# require "pry"
|
11
|
-
# Pry.start
|
12
|
-
|
13
|
-
require "irb"
|
14
|
-
IRB.start(__FILE__)
|
data/bin/ubcbooker
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
module Ubcbooker
|
2
|
+
class Account
|
3
|
+
attr_accessor :username, :password
|
4
|
+
|
5
|
+
def initialize
|
6
|
+
@config_path = File.expand_path("../config.yml", __FILE__)
|
7
|
+
@account_info = YAML.load_file(@config_path)
|
8
|
+
@username = @account_info["username"]
|
9
|
+
@password = @account_info["password"]
|
10
|
+
end
|
11
|
+
|
12
|
+
def write(username, password)
|
13
|
+
@account["username"] = username
|
14
|
+
@account["password"] = password
|
15
|
+
new_yml = YAML.dump(@account)
|
16
|
+
open(@config_path, "w") { |f| f.write(new_yml) }
|
17
|
+
@account = YAML.load_file(@config_path)
|
18
|
+
end
|
19
|
+
|
20
|
+
def print_supported_departments
|
21
|
+
puts "Supported department options in #{Ubcbooker::VERSION}:"
|
22
|
+
BOOKING_URL.keys.each do |d|
|
23
|
+
puts " - #{d}"
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def defined?
|
28
|
+
return @account["username"] != "hoge" && @account["password"] != "hoge"
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,156 @@
|
|
1
|
+
module Ubcbooker
|
2
|
+
class CLI
|
3
|
+
attr_accessor :options
|
4
|
+
|
5
|
+
def initialize
|
6
|
+
@config = Ubcbooker::Config.new
|
7
|
+
@options = nil
|
8
|
+
end
|
9
|
+
|
10
|
+
def ask_config
|
11
|
+
print "Your CWL username: "
|
12
|
+
username = gets.chomp
|
13
|
+
print "Your CWL password: "
|
14
|
+
# Hide the password input
|
15
|
+
password = STDIN.noecho(&:gets).chomp
|
16
|
+
@config.write(username, password)
|
17
|
+
puts
|
18
|
+
end
|
19
|
+
|
20
|
+
def parse_options
|
21
|
+
|
22
|
+
# This will hold the options we parse
|
23
|
+
options = {
|
24
|
+
name: nil,
|
25
|
+
date: nil,
|
26
|
+
time: nil,
|
27
|
+
department: nil,
|
28
|
+
}
|
29
|
+
|
30
|
+
# TODO: Change department to building
|
31
|
+
OptionParser.new do |parser|
|
32
|
+
parser.on("-b", "--building BUILDING", String, "Specify which department to book rooms from") do |v|
|
33
|
+
if CLI::Validator.is_valid_department(v)
|
34
|
+
options[:department] = v
|
35
|
+
else
|
36
|
+
raise Ubcbooker::Error::UnsupportedDepartment.new(v)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
parser.on("-d", "--date DATE", String, "Specify date to book rooms for (MM/DD)") do |v|
|
41
|
+
if CLI::Validator.is_valid_date(v)
|
42
|
+
options[:date] = v
|
43
|
+
else
|
44
|
+
raise Ubcbooker::Error::UnsupportedDate.new(v)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
parser.on("-h", "--help", "Show this help message") do
|
49
|
+
puts parser
|
50
|
+
puts
|
51
|
+
puts "ex. Book a room in CS from 11am to 1pm on March 5th with the name 'Study Group'"
|
52
|
+
puts " $>ubcbooker -b cs -n 'Study Group' -d 03/05 -t 11:00-13:00"
|
53
|
+
exit(0)
|
54
|
+
end
|
55
|
+
|
56
|
+
parser.on("-l", "--list", "List supported departments") do |v|
|
57
|
+
@config.print_supported_departments
|
58
|
+
exit(0)
|
59
|
+
end
|
60
|
+
|
61
|
+
parser.on("-n", "--name NAME", String, "Name of the booking") do |v|
|
62
|
+
if CLI::Validator.is_valid_name(v)
|
63
|
+
options[:name] = v
|
64
|
+
else
|
65
|
+
raise Ubcbooker::Error::ProfaneName.new(v)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
parser.on("-t", "--time TIME", String,
|
69
|
+
"Specify time to book rooms for (HH:MM-HH:MM)") do |v|
|
70
|
+
if CLI::Validator.is_valid_time(v)
|
71
|
+
options[:time] = v
|
72
|
+
else
|
73
|
+
raise Ubcbooker::Error::UnsupportedTime.new(v)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
parser.on("-u", "--update", "Update username and password") do |v|
|
78
|
+
ask_config
|
79
|
+
exit(0)
|
80
|
+
end
|
81
|
+
|
82
|
+
parser.on("-v", "--version", "Show version") do |v|
|
83
|
+
puts Ubcbooker::VERSION
|
84
|
+
exit(0)
|
85
|
+
end
|
86
|
+
end.parse!
|
87
|
+
|
88
|
+
spinner = get_spinner("Verifying inputs")
|
89
|
+
spinner.success("Done!") # Stop animation
|
90
|
+
|
91
|
+
if CLI::Validator.is_required_missing(options)
|
92
|
+
raise OptionParser::MissingArgument
|
93
|
+
end
|
94
|
+
|
95
|
+
return options
|
96
|
+
end
|
97
|
+
|
98
|
+
def get_options
|
99
|
+
option_errors = [
|
100
|
+
Ubcbooker::Error::UnsupportedDepartment,
|
101
|
+
Ubcbooker::Error::UnsupportedTime,
|
102
|
+
Ubcbooker::Error::UnsupportedDate,
|
103
|
+
Ubcbooker::Error::ProfaneName,
|
104
|
+
]
|
105
|
+
|
106
|
+
begin
|
107
|
+
return parse_options
|
108
|
+
rescue OptionParser::MissingArgument
|
109
|
+
puts "Error: Missing Option\n".red <<
|
110
|
+
"One or more of required option are missing values\n" <<
|
111
|
+
"Please check if options -b, -d, -n, and -t all have values passed"
|
112
|
+
exit(1)
|
113
|
+
rescue *option_errors => e
|
114
|
+
puts e.message
|
115
|
+
exit(1)
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
def get_department_scraper(department)
|
120
|
+
case department
|
121
|
+
when "cs"
|
122
|
+
return Ubcbooker::Scraper::Cs
|
123
|
+
when "sauder_ugrad"
|
124
|
+
return Ubcbooker::Scraper::SauderUgrad
|
125
|
+
else
|
126
|
+
raise Ubcbooker::Error::UnsupportedDepartment.new(department)
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
def get_scraper(department, username, password)
|
131
|
+
scraper_client = get_department_scraper(department)
|
132
|
+
return scraper_client.new(username, password)
|
133
|
+
end
|
134
|
+
|
135
|
+
def get_spinner(text)
|
136
|
+
spinner = ::TTY::Spinner.new("[:spinner] #{text} ... ", format: :dots)
|
137
|
+
spinner.auto_spin # Automatic animation with default interval
|
138
|
+
return spinner
|
139
|
+
end
|
140
|
+
|
141
|
+
def start
|
142
|
+
@options = get_options
|
143
|
+
ask_config if !@config.defined?
|
144
|
+
|
145
|
+
@client = get_scraper(@options[:department],
|
146
|
+
@config.account["username"],
|
147
|
+
@config.account["password"])
|
148
|
+
begin
|
149
|
+
room_id = @client.book(@options)
|
150
|
+
puts "Success! #{room_id} is booked".green
|
151
|
+
rescue Ubcbooker::Error::NoAvailableRoom => e
|
152
|
+
puts e.message
|
153
|
+
end
|
154
|
+
end
|
155
|
+
end
|
156
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
module Ubcbooker
|
2
|
+
class CLI
|
3
|
+
module Validator
|
4
|
+
def self.is_valid_department(d)
|
5
|
+
return BOOKING_URL.keys.include?(d.to_sym)
|
6
|
+
end
|
7
|
+
|
8
|
+
def self.is_valid_date(d)
|
9
|
+
date = nil
|
10
|
+
begin
|
11
|
+
date = Date.parse(d)
|
12
|
+
# Expect MM/DD
|
13
|
+
rescue ArgumentError
|
14
|
+
return false
|
15
|
+
end
|
16
|
+
return /^\d\d\/\d\d$/.match?(d) && # Match format
|
17
|
+
date.weekday? && # Not on weekend
|
18
|
+
!date.past? && # Not in the past
|
19
|
+
(date < Date.today + 7) # Within a week
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.is_valid_time(t)
|
23
|
+
if /^\d\d:\d\d-\d\d:\d\d$/.match?(t)
|
24
|
+
times = t.split("-")
|
25
|
+
times.each do |time|
|
26
|
+
begin
|
27
|
+
DateTime.parse(time)
|
28
|
+
# Expect HH:MM
|
29
|
+
rescue ArgumentError
|
30
|
+
return false
|
31
|
+
end
|
32
|
+
end
|
33
|
+
return true
|
34
|
+
else
|
35
|
+
return false
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def self.is_required_missing(options)
|
40
|
+
return options[:name].nil? || options[:date].nil? ||
|
41
|
+
options[:time].nil? || options[:department].nil?
|
42
|
+
end
|
43
|
+
|
44
|
+
# False if the name contains any profanity
|
45
|
+
def self.is_valid_name(name)
|
46
|
+
return !Obscenity.profane?(name)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
class String
|
2
|
+
def black; "\e[30m#{self}\e[0m" end
|
3
|
+
def red; "\e[31m#{self}\e[0m" end
|
4
|
+
def green; "\e[32m#{self}\e[0m" end
|
5
|
+
def brown; "\e[33m#{self}\e[0m" end
|
6
|
+
def blue; "\e[34m#{self}\e[0m" end
|
7
|
+
def magenta; "\e[35m#{self}\e[0m" end
|
8
|
+
def cyan; "\e[36m#{self}\e[0m" end
|
9
|
+
def gray; "\e[37m#{self}\e[0m" end
|
10
|
+
|
11
|
+
def bold; "\e[1m#{self}\e[22m" end
|
12
|
+
def italic; "\e[3m#{self}\e[23m" end
|
13
|
+
def underline; "\e[4m#{self}\e[24m" end
|
14
|
+
def blink; "\e[5m#{self}\e[25m" end
|
15
|
+
def reverse_color; "\e[7m#{self}\e[27m" end
|
16
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
module Ubcbooker
|
2
|
+
class Config
|
3
|
+
attr_accessor :account
|
4
|
+
|
5
|
+
def initialize
|
6
|
+
@config_path = File.expand_path("../config.yml", __FILE__)
|
7
|
+
if !File.exist?(@config_path)
|
8
|
+
create_config_file(@config_path)
|
9
|
+
end
|
10
|
+
|
11
|
+
@account = YAML.load_file(@config_path)
|
12
|
+
end
|
13
|
+
|
14
|
+
def create_config_file(config_path)
|
15
|
+
File.open(config_path, "w") do |f|
|
16
|
+
f.write("---\nusername: sample\npassword: sample")
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def write(username, password)
|
21
|
+
@account["username"] = username
|
22
|
+
@account["password"] = password
|
23
|
+
new_yml = YAML.dump(@account)
|
24
|
+
open(@config_path, "w") { |f| f.write(new_yml) }
|
25
|
+
@account = YAML.load_file(@config_path)
|
26
|
+
end
|
27
|
+
|
28
|
+
def print_supported_departments
|
29
|
+
puts "Supported department options in #{Ubcbooker::VERSION}:"
|
30
|
+
BOOKING_URL.keys.each do |d|
|
31
|
+
puts " - #{d}"
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def defined?
|
36
|
+
return @account["username"] != "sample" && @account["password"] != "sample"
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
module Ubcbooker
|
2
|
+
module Error
|
3
|
+
class UnsupportedDepartment < StandardError
|
4
|
+
attr_reader :message
|
5
|
+
def initialize(department = "unknown")
|
6
|
+
@message = "\"#{department}\" is an unsupported department\n".red <<
|
7
|
+
"Check supported departments with `ubcbooker -l`"
|
8
|
+
super
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
class UnsupportedDate < StandardError
|
13
|
+
attr_reader :message, :date
|
14
|
+
def initialize(date = "unknown")
|
15
|
+
@date = date
|
16
|
+
@message = "Error: Unsupported Date\n".red <<
|
17
|
+
"Date must not be:\n" <<
|
18
|
+
" - in the past\n" <<
|
19
|
+
" - in a weekend\n" <<
|
20
|
+
" - beyond a week\n" <<
|
21
|
+
"Please check if the time is in the format of MM/DD\n" <<
|
22
|
+
"Ex. 03/05"
|
23
|
+
super
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
class UnsupportedTime < StandardError
|
28
|
+
attr_reader :message, :time
|
29
|
+
def initialize(time = "unknown")
|
30
|
+
@time = time
|
31
|
+
@message = "Error: Unsupported Time\n".red <<
|
32
|
+
"Please check if the time is in the format of HH:MM-HH:MM\n" <<
|
33
|
+
"Ex. 11:00-13:00"
|
34
|
+
super
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
class ProfaneName < StandardError
|
39
|
+
attr_reader :message, :name
|
40
|
+
def initialize(name = "unknown")
|
41
|
+
@name = name
|
42
|
+
@message = "Error: Name includes profanity\n".red <<
|
43
|
+
"Remember that other students might see the booking name\n" <<
|
44
|
+
"Please try again with a different name"
|
45
|
+
super
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
class NoAvailableRoom < StandardError
|
50
|
+
attr_reader :message, :time_range
|
51
|
+
def initialize(time_range)
|
52
|
+
@time_range = time_range
|
53
|
+
@message = "Error: No Available Room\n".red <<
|
54
|
+
"There are no room available for #{time_range} range\n" <<
|
55
|
+
"Please try again with a different time range"
|
56
|
+
super
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
class LoginFailed < StandardError
|
61
|
+
attr_reader :message
|
62
|
+
def initialize
|
63
|
+
@message = "\nLogin Failed :/\n".red <<
|
64
|
+
"Please try logging in with a different username or password\n" <<
|
65
|
+
"You can use `-u` flag to update saved accout info"
|
66
|
+
super
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
module Ubcbooker
|
2
|
+
module Scraper
|
3
|
+
class BaseScraper
|
4
|
+
def initialize(username, password)
|
5
|
+
@agent = Mechanize.new do |agent|
|
6
|
+
agent.user_agent_alias = "Linux Mozilla"
|
7
|
+
end
|
8
|
+
@username = username
|
9
|
+
@password = password
|
10
|
+
end
|
11
|
+
|
12
|
+
def is_logged_in(page)
|
13
|
+
page_body = Nokogiri::HTML(page.body)
|
14
|
+
login_status_text = page_body.css("p").first.text
|
15
|
+
return !login_status_text.include?("Login Failed")
|
16
|
+
end
|
17
|
+
|
18
|
+
def populate_account_info(login_page)
|
19
|
+
username_feild = login_page.form.field_with(name: "j_username")
|
20
|
+
username_feild.value = @username
|
21
|
+
password_field = login_page.form.field_with(name: "j_password")
|
22
|
+
password_field.value = @password
|
23
|
+
redirect_page = login_page.form.submit
|
24
|
+
return redirect_page.form.submit
|
25
|
+
end
|
26
|
+
|
27
|
+
# Do login for UBC CWL system
|
28
|
+
def login_ubc_cwl(login_page)
|
29
|
+
begin
|
30
|
+
after_login_page = populate_account_info(login_page)
|
31
|
+
if is_logged_in(after_login_page)
|
32
|
+
return after_login_page
|
33
|
+
else
|
34
|
+
raise Ubcbooker::Error::LoginFailed
|
35
|
+
end
|
36
|
+
rescue Ubcbooker::Error::LoginFailed => e
|
37
|
+
puts e.message
|
38
|
+
exit(1)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,175 @@
|
|
1
|
+
module Ubcbooker
|
2
|
+
module Scraper
|
3
|
+
class Cs < BaseScraper
|
4
|
+
CS_ROOM_BASE_URL = "https://my.cs.ubc.ca/space/"
|
5
|
+
CS_BOOK_URL = "https://my.cs.ubc.ca/node/add/pr-booking"
|
6
|
+
CS_ROOMS = ["X139", "X141", "X151", "X153", # 1st Floor
|
7
|
+
"X237", "X239", "X241", # 2nd Floor
|
8
|
+
"X337", "X339", "X341"] # 3rd Floor
|
9
|
+
|
10
|
+
def book(options)
|
11
|
+
login
|
12
|
+
title = options[:name]
|
13
|
+
book_date = Date.parse(options[:date])
|
14
|
+
book_slot = get_time_slot(options[:time])
|
15
|
+
|
16
|
+
room_pages = batch_request(CS_ROOMS, book_date)
|
17
|
+
room_pages.select! { |r| is_available(r, book_date, book_slot) }
|
18
|
+
|
19
|
+
if room_pages.any?
|
20
|
+
# TODO: If -c CHOOSE option then run CLI-UI here
|
21
|
+
room = room_pages.first # Choose the first available room
|
22
|
+
submit_booking(room, title, book_date, book_slot)
|
23
|
+
return get_room_page_id(room)
|
24
|
+
else
|
25
|
+
raise Ubcbooker::Error::NoAvailableRoom.new(options[:time])
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def login
|
30
|
+
spinner = get_spinner("Logging into CWL")
|
31
|
+
booking_url = BOOKING_URL[:cs]
|
32
|
+
@agent.get(booking_url) do |page|
|
33
|
+
login_page = page.link_with(text: "CWL Login Redirect").click
|
34
|
+
login_ubc_cwl(login_page)
|
35
|
+
end
|
36
|
+
spinner.success("Done!") # Stop animation
|
37
|
+
end
|
38
|
+
|
39
|
+
def submit_booking(room, title, book_date, book_slot)
|
40
|
+
spinner = get_spinner("Submitting booking request")
|
41
|
+
booking_form = @agent.get(CS_BOOK_URL).forms[1]
|
42
|
+
booking_form["title"] = title
|
43
|
+
book_date_str = book_date.strftime("%Y/%m/%d") # ex 2018/03/08
|
44
|
+
select_room_option(booking_form, room)
|
45
|
+
booking_form["field_date[und][0][value][date]"] = book_date_str
|
46
|
+
booking_form["field_date[und][0][value][time]"] = time_to_ampm(book_slot.min)
|
47
|
+
booking_form["field_date[und][0][value2][date]"] = book_date_str
|
48
|
+
booking_form["field_date[und][0][value2][time]"] = time_to_ampm(book_slot.max)
|
49
|
+
spinner.success("Done!")
|
50
|
+
booking_form.submit
|
51
|
+
end
|
52
|
+
|
53
|
+
# Select the form otpion with right room id
|
54
|
+
def select_room_option(booking_form, page)
|
55
|
+
room_id = get_room_page_id(page)
|
56
|
+
select_options = booking_form.field_with(name: "field_space_project_room[und]").options
|
57
|
+
select_options.each do |o|
|
58
|
+
if o.text.include?(room_id)
|
59
|
+
o.click
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def get_room_page_id(page)
|
65
|
+
return page.form.action.split("/").last
|
66
|
+
end
|
67
|
+
|
68
|
+
def batch_request(room_list, book_date)
|
69
|
+
cookie = @agent.cookies.join("; ")
|
70
|
+
spinner = get_spinner("Checking room availabilities")
|
71
|
+
|
72
|
+
hydra = Typhoeus::Hydra.new
|
73
|
+
requests = room_list.map do |room_id|
|
74
|
+
room_url = get_room_cal_url(book_date, room_id)
|
75
|
+
request = Typhoeus::Request.new(room_url, headers: { Cookie: cookie })
|
76
|
+
hydra.queue(request)
|
77
|
+
request
|
78
|
+
end
|
79
|
+
hydra.run # Start requests
|
80
|
+
spinner.success("Done!")
|
81
|
+
return typhoeus_to_mechanize(requests)
|
82
|
+
end
|
83
|
+
|
84
|
+
def get_room_cal_url(book_date, room_id)
|
85
|
+
if CS_ROOMS.include?(room_id)
|
86
|
+
room_url = CS_ROOM_BASE_URL + "ICCS" + room_id
|
87
|
+
if is_next_month(book_date)
|
88
|
+
today = Date.today
|
89
|
+
month_query = "?month=" + today.year + "-" + (today.month + 1)
|
90
|
+
return room_url + month_query
|
91
|
+
else
|
92
|
+
return room_url
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
# Turn typhoneus obj to mechanize page obj
|
98
|
+
def typhoeus_to_mechanize(requests)
|
99
|
+
pages = requests.map do |request|
|
100
|
+
html = request.response.body
|
101
|
+
Mechanize::Page.new(nil, {"ontent-type" => "text/html"}, html, nil, @agent)
|
102
|
+
end
|
103
|
+
return pages
|
104
|
+
end
|
105
|
+
|
106
|
+
def is_next_month(date)
|
107
|
+
return date.month != Date.today.month
|
108
|
+
end
|
109
|
+
|
110
|
+
def is_available(room_page, book_date, book_slot)
|
111
|
+
slot_booked = get_slot_booked(room_page, book_date)
|
112
|
+
return !is_slot_booked(slot_booked, book_slot)
|
113
|
+
end
|
114
|
+
|
115
|
+
def is_slot_booked(slot_booked, book_slot)
|
116
|
+
booked = false
|
117
|
+
slot_booked.each do |s|
|
118
|
+
if s.include?(book_slot)
|
119
|
+
booked = true
|
120
|
+
end
|
121
|
+
end
|
122
|
+
return booked
|
123
|
+
end
|
124
|
+
|
125
|
+
def get_slot_booked(room_page, book_date)
|
126
|
+
day_div_id = get_date_div_id(book_date)
|
127
|
+
day_div = room_page.search("td##{day_div_id}").first
|
128
|
+
slot_num = day_div.search("div.item").size
|
129
|
+
start_times = day_div.search("span.date-display-start")
|
130
|
+
end_times = day_div.search("span.date-display-end")
|
131
|
+
|
132
|
+
slot_booked = []
|
133
|
+
(0..slot_num-1).each do |i|
|
134
|
+
slot_start = ampm_to_time(start_times[i].text) # 01:00
|
135
|
+
slot_end = ampm_to_time(end_times[i].text) # 05:00
|
136
|
+
slot_booked.push((slot_start..slot_end))
|
137
|
+
end
|
138
|
+
return slot_booked
|
139
|
+
end
|
140
|
+
|
141
|
+
def get_date_div_id(date)
|
142
|
+
date_div_base = "calendar_space_entity_project_room-"
|
143
|
+
date_div_base += "#{date.year}-#{date.strftime("%m")}-#{date.strftime("%d")}-0"
|
144
|
+
return date_div_base
|
145
|
+
end
|
146
|
+
|
147
|
+
def get_spinner(text)
|
148
|
+
spinner = ::TTY::Spinner.new("[:spinner] #{text} ... ", format: :dots)
|
149
|
+
spinner.auto_spin # Automatic animation with default interval
|
150
|
+
return spinner
|
151
|
+
end
|
152
|
+
|
153
|
+
# =========================
|
154
|
+
# Time operations
|
155
|
+
|
156
|
+
# Expect HH:MM-HH:MM
|
157
|
+
def get_time_slot(time_str)
|
158
|
+
times = time_str.split("-")
|
159
|
+
return (times[0]..times[1])
|
160
|
+
end
|
161
|
+
|
162
|
+
# 05:00pm -> 17:99
|
163
|
+
def ampm_to_time(str)
|
164
|
+
time = Time.parse(str)
|
165
|
+
return time.strftime("%H:%M")
|
166
|
+
end
|
167
|
+
|
168
|
+
# 17:00 -> 05:00pm
|
169
|
+
def time_to_ampm(str)
|
170
|
+
time = Time.parse(str)
|
171
|
+
return time.strftime("%I:%M%P")
|
172
|
+
end
|
173
|
+
end
|
174
|
+
end
|
175
|
+
end
|
data/lib/ubcbooker/version.rb
CHANGED
data/lib/ubcbooker.rb
CHANGED
@@ -1,50 +1,24 @@
|
|
1
|
-
require "ubcbooker/version"
|
2
|
-
require "io/console" # For handling password
|
3
|
-
require "mechanize"
|
4
1
|
require "pry"
|
2
|
+
require "business_time"
|
3
|
+
require "tty-spinner"
|
4
|
+
require "mechanize"
|
5
|
+
require "typhoeus"
|
6
|
+
require "obscenity"
|
7
|
+
require "io/console" # For handling password
|
8
|
+
require "optparse"
|
9
|
+
require "yaml"
|
5
10
|
|
6
11
|
module Ubcbooker
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
# - Feature to save password
|
12
|
-
# - Use Thor for CLI
|
13
|
-
# - Divide up to files
|
14
|
-
|
15
|
-
class User
|
16
|
-
attr_accessor :username, :password
|
17
|
-
end
|
18
|
-
|
19
|
-
print "CWL username: "
|
20
|
-
username = gets.chomp
|
21
|
-
print "CWL password: "
|
22
|
-
# Hide the password input
|
23
|
-
password = STDIN.noecho(&:gets).chomp
|
24
|
-
puts
|
25
|
-
|
26
|
-
a = Mechanize.new do |agent|
|
27
|
-
agent.user_agent_alias = "Linux Mozilla"
|
28
|
-
end
|
29
|
-
|
30
|
-
# What happens if I'm already logged in here?
|
31
|
-
# Maybe check by seeing if the CWL link pops up after GET
|
32
|
-
|
33
|
-
# Do login for UBC CWL system
|
34
|
-
def self.login_ubc_cwl(login_page)
|
35
|
-
username_feild = login_page.form.field_with(name => "j_username")
|
36
|
-
username_feild.value = username
|
37
|
-
password_field = login_page.form.field_with(name => "j_password")
|
38
|
-
password_field.value = password
|
39
|
-
redirect_page = login_page.form.submit
|
40
|
-
return reditect_page.form.submit
|
41
|
-
end
|
42
|
-
|
43
|
-
puts "Requesting the page ..."
|
44
|
-
a.get("https://my.cs.ubc.ca/docs/project-rooms-and-dlc") do |page|
|
45
|
-
login_page = page.link_with(text => "CWL Login Redirect").click
|
46
|
-
login_ubc_cwl(login_page)
|
47
|
-
binding.pry
|
48
|
-
end
|
49
|
-
|
12
|
+
BOOKING_URL = {
|
13
|
+
cs: "https://my.cs.ubc.ca/docs/project-rooms-and-dlc",
|
14
|
+
# sauder_ugrad: "https://booking.sauder.ubc.ca/ugr/cwl-login",
|
15
|
+
}
|
50
16
|
end
|
17
|
+
|
18
|
+
require "ubcbooker/version"
|
19
|
+
require "ubcbooker/error"
|
20
|
+
require "ubcbooker/color"
|
21
|
+
require "ubcbooker/config"
|
22
|
+
require "ubcbooker/scrapers/scraper"
|
23
|
+
require "ubcbooker/cli_validator"
|
24
|
+
require "ubcbooker/cli"
|
data/ubcbooker.gemspec
CHANGED
@@ -16,15 +16,18 @@ Gem::Specification.new do |spec|
|
|
16
16
|
spec.files = `git ls-files -z`.split("\x0").reject do |f|
|
17
17
|
f.match(%r{^(test|spec|features)/})
|
18
18
|
end
|
19
|
-
spec.
|
20
|
-
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
19
|
+
spec.executables = ["ubcbooker"]
|
21
20
|
spec.require_paths = ["lib"]
|
22
21
|
|
23
22
|
spec.add_dependency "mechanize", "~> 2.7"
|
23
|
+
spec.add_dependency "business_time", "~> 0.9.3"
|
24
|
+
spec.add_dependency "typhoeus", "~> 1.1"
|
25
|
+
spec.add_dependency "obscenity", "~> 1.0.2"
|
26
|
+
spec.add_dependency "tty-spinner", "~> 0.7.0"
|
24
27
|
|
25
|
-
spec.add_development_dependency "bundler", "~> 1.16"
|
26
|
-
spec.add_development_dependency "pry", "0.11.3"
|
27
28
|
spec.add_development_dependency "rake", "~> 10.0"
|
28
29
|
spec.add_development_dependency "rspec", "~> 3.0"
|
30
|
+
spec.add_development_dependency "pry", "~> 0.11.3"
|
31
|
+
spec.add_development_dependency "pry-byebug", "~> 3.4"
|
29
32
|
spec.add_development_dependency "rubocop-github", "~> 0.8.1"
|
30
33
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: ubcbooker
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1
|
4
|
+
version: 0.2.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Atsushi Yamamoto
|
8
8
|
autorequire:
|
9
|
-
bindir:
|
9
|
+
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2018-
|
11
|
+
date: 2018-03-11 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: mechanize
|
@@ -25,33 +25,61 @@ dependencies:
|
|
25
25
|
- !ruby/object:Gem::Version
|
26
26
|
version: '2.7'
|
27
27
|
- !ruby/object:Gem::Dependency
|
28
|
-
name:
|
28
|
+
name: business_time
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
30
30
|
requirements:
|
31
31
|
- - "~>"
|
32
32
|
- !ruby/object:Gem::Version
|
33
|
-
version:
|
34
|
-
type: :
|
33
|
+
version: 0.9.3
|
34
|
+
type: :runtime
|
35
35
|
prerelease: false
|
36
36
|
version_requirements: !ruby/object:Gem::Requirement
|
37
37
|
requirements:
|
38
38
|
- - "~>"
|
39
39
|
- !ruby/object:Gem::Version
|
40
|
-
version:
|
40
|
+
version: 0.9.3
|
41
41
|
- !ruby/object:Gem::Dependency
|
42
|
-
name:
|
42
|
+
name: typhoeus
|
43
43
|
requirement: !ruby/object:Gem::Requirement
|
44
44
|
requirements:
|
45
|
-
- -
|
45
|
+
- - "~>"
|
46
46
|
- !ruby/object:Gem::Version
|
47
|
-
version:
|
48
|
-
type: :
|
47
|
+
version: '1.1'
|
48
|
+
type: :runtime
|
49
49
|
prerelease: false
|
50
50
|
version_requirements: !ruby/object:Gem::Requirement
|
51
51
|
requirements:
|
52
|
-
- -
|
52
|
+
- - "~>"
|
53
53
|
- !ruby/object:Gem::Version
|
54
|
-
version:
|
54
|
+
version: '1.1'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: obscenity
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: 1.0.2
|
62
|
+
type: :runtime
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: 1.0.2
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: tty-spinner
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: 0.7.0
|
76
|
+
type: :runtime
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: 0.7.0
|
55
83
|
- !ruby/object:Gem::Dependency
|
56
84
|
name: rake
|
57
85
|
requirement: !ruby/object:Gem::Requirement
|
@@ -80,6 +108,34 @@ dependencies:
|
|
80
108
|
- - "~>"
|
81
109
|
- !ruby/object:Gem::Version
|
82
110
|
version: '3.0'
|
111
|
+
- !ruby/object:Gem::Dependency
|
112
|
+
name: pry
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
114
|
+
requirements:
|
115
|
+
- - "~>"
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: 0.11.3
|
118
|
+
type: :development
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
requirements:
|
122
|
+
- - "~>"
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
version: 0.11.3
|
125
|
+
- !ruby/object:Gem::Dependency
|
126
|
+
name: pry-byebug
|
127
|
+
requirement: !ruby/object:Gem::Requirement
|
128
|
+
requirements:
|
129
|
+
- - "~>"
|
130
|
+
- !ruby/object:Gem::Version
|
131
|
+
version: '3.4'
|
132
|
+
type: :development
|
133
|
+
prerelease: false
|
134
|
+
version_requirements: !ruby/object:Gem::Requirement
|
135
|
+
requirements:
|
136
|
+
- - "~>"
|
137
|
+
- !ruby/object:Gem::Version
|
138
|
+
version: '3.4'
|
83
139
|
- !ruby/object:Gem::Dependency
|
84
140
|
name: rubocop-github
|
85
141
|
requirement: !ruby/object:Gem::Requirement
|
@@ -97,7 +153,8 @@ dependencies:
|
|
97
153
|
description: CLI tool to book project rooms in UBC
|
98
154
|
email:
|
99
155
|
- hi@atsushi.me
|
100
|
-
executables:
|
156
|
+
executables:
|
157
|
+
- ubcbooker
|
101
158
|
extensions: []
|
102
159
|
extra_rdoc_files: []
|
103
160
|
files:
|
@@ -107,13 +164,23 @@ files:
|
|
107
164
|
- ".travis.yml"
|
108
165
|
- CODE_OF_CONDUCT.md
|
109
166
|
- Gemfile
|
110
|
-
- Gemfile.lock
|
111
167
|
- LICENSE
|
112
168
|
- README.md
|
113
169
|
- Rakefile
|
114
170
|
- bin/console
|
115
171
|
- bin/setup
|
172
|
+
- bin/ubcbooker
|
116
173
|
- lib/ubcbooker.rb
|
174
|
+
- lib/ubcbooker/account.rb
|
175
|
+
- lib/ubcbooker/cli.rb
|
176
|
+
- lib/ubcbooker/cli_validator.rb
|
177
|
+
- lib/ubcbooker/color.rb
|
178
|
+
- lib/ubcbooker/config.rb
|
179
|
+
- lib/ubcbooker/error.rb
|
180
|
+
- lib/ubcbooker/sample.yml
|
181
|
+
- lib/ubcbooker/scrapers/base_scraper.rb
|
182
|
+
- lib/ubcbooker/scrapers/cs.rb
|
183
|
+
- lib/ubcbooker/scrapers/scraper.rb
|
117
184
|
- lib/ubcbooker/version.rb
|
118
185
|
- ubcbooker.gemspec
|
119
186
|
homepage: https://github.com/jumbosushi/ubcbooker
|
data/Gemfile.lock
DELETED
@@ -1,86 +0,0 @@
|
|
1
|
-
PATH
|
2
|
-
remote: .
|
3
|
-
specs:
|
4
|
-
ubcbooker (0.1.0)
|
5
|
-
mechanize (~> 2.7)
|
6
|
-
|
7
|
-
GEM
|
8
|
-
remote: https://rubygems.org/
|
9
|
-
specs:
|
10
|
-
ast (2.3.0)
|
11
|
-
coderay (1.1.2)
|
12
|
-
diff-lcs (1.3)
|
13
|
-
domain_name (0.5.20170404)
|
14
|
-
unf (>= 0.0.5, < 1.0.0)
|
15
|
-
http-cookie (1.0.3)
|
16
|
-
domain_name (~> 0.5)
|
17
|
-
mechanize (2.7.5)
|
18
|
-
domain_name (~> 0.5, >= 0.5.1)
|
19
|
-
http-cookie (~> 1.0)
|
20
|
-
mime-types (>= 1.17.2)
|
21
|
-
net-http-digest_auth (~> 1.1, >= 1.1.1)
|
22
|
-
net-http-persistent (~> 2.5, >= 2.5.2)
|
23
|
-
nokogiri (~> 1.6)
|
24
|
-
ntlm-http (~> 0.1, >= 0.1.1)
|
25
|
-
webrobots (>= 0.0.9, < 0.2)
|
26
|
-
method_source (0.9.0)
|
27
|
-
mime-types (3.1)
|
28
|
-
mime-types-data (~> 3.2015)
|
29
|
-
mime-types-data (3.2016.0521)
|
30
|
-
mini_portile2 (2.3.0)
|
31
|
-
net-http-digest_auth (1.4.1)
|
32
|
-
net-http-persistent (2.9.4)
|
33
|
-
nokogiri (1.8.1)
|
34
|
-
mini_portile2 (~> 2.3.0)
|
35
|
-
ntlm-http (0.1.1)
|
36
|
-
parallel (1.12.1)
|
37
|
-
parser (2.4.0.2)
|
38
|
-
ast (~> 2.3)
|
39
|
-
powerpack (0.1.1)
|
40
|
-
pry (0.11.3)
|
41
|
-
coderay (~> 1.1.0)
|
42
|
-
method_source (~> 0.9.0)
|
43
|
-
rainbow (3.0.0)
|
44
|
-
rake (10.5.0)
|
45
|
-
rspec (3.6.0)
|
46
|
-
rspec-core (~> 3.6.0)
|
47
|
-
rspec-expectations (~> 3.6.0)
|
48
|
-
rspec-mocks (~> 3.6.0)
|
49
|
-
rspec-core (3.6.0)
|
50
|
-
rspec-support (~> 3.6.0)
|
51
|
-
rspec-expectations (3.6.0)
|
52
|
-
diff-lcs (>= 1.2.0, < 2.0)
|
53
|
-
rspec-support (~> 3.6.0)
|
54
|
-
rspec-mocks (3.6.0)
|
55
|
-
diff-lcs (>= 1.2.0, < 2.0)
|
56
|
-
rspec-support (~> 3.6.0)
|
57
|
-
rspec-support (3.6.0)
|
58
|
-
rubocop (0.52.1)
|
59
|
-
parallel (~> 1.10)
|
60
|
-
parser (>= 2.4.0.2, < 3.0)
|
61
|
-
powerpack (~> 0.1)
|
62
|
-
rainbow (>= 2.2.2, < 4.0)
|
63
|
-
ruby-progressbar (~> 1.7)
|
64
|
-
unicode-display_width (~> 1.0, >= 1.0.1)
|
65
|
-
rubocop-github (0.8.1)
|
66
|
-
rubocop (~> 0.51)
|
67
|
-
ruby-progressbar (1.9.0)
|
68
|
-
unf (0.1.4)
|
69
|
-
unf_ext
|
70
|
-
unf_ext (0.0.7.4)
|
71
|
-
unicode-display_width (1.3.0)
|
72
|
-
webrobots (0.1.2)
|
73
|
-
|
74
|
-
PLATFORMS
|
75
|
-
ruby
|
76
|
-
|
77
|
-
DEPENDENCIES
|
78
|
-
bundler (~> 1.16)
|
79
|
-
pry (= 0.11.3)
|
80
|
-
rake (~> 10.0)
|
81
|
-
rspec (~> 3.0)
|
82
|
-
rubocop-github (~> 0.8.1)
|
83
|
-
ubcbooker!
|
84
|
-
|
85
|
-
BUNDLED WITH
|
86
|
-
1.16.1
|