ubcbooker 0.1.0 → 0.2.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.
- 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
|