milton 1.0.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/History.txt +6 -0
- data/Manifest.txt +8 -0
- data/README.txt +63 -0
- data/Rakefile +12 -0
- data/bin/milton +6 -0
- data/bin/recorder +27 -0
- data/lib/milton.rb +293 -0
- data/test/test_milton.rb +0 -0
- metadata +86 -0
data/History.txt
ADDED
data/Manifest.txt
ADDED
data/README.txt
ADDED
@@ -0,0 +1,63 @@
|
|
1
|
+
= milton
|
2
|
+
|
3
|
+
* http://seattlerb.rubyforge.org/milton
|
4
|
+
|
5
|
+
== DESCRIPTION:
|
6
|
+
|
7
|
+
Milton fills out your ADP ezLaborManager timesheet
|
8
|
+
|
9
|
+
== FEATURES/PROBLEMS:
|
10
|
+
|
11
|
+
* Fills out timesheets for arbitrary weeks
|
12
|
+
* Does not account for time off
|
13
|
+
|
14
|
+
== SYNOPSIS:
|
15
|
+
|
16
|
+
To fill out the current week:
|
17
|
+
|
18
|
+
milton
|
19
|
+
|
20
|
+
To view your timesheet for the current week:
|
21
|
+
|
22
|
+
milton --view
|
23
|
+
|
24
|
+
To fill out a timesheet for an arbitrary week:
|
25
|
+
|
26
|
+
milton --date=02/25/2009
|
27
|
+
|
28
|
+
To view a timesheet for an arbitrary week:
|
29
|
+
|
30
|
+
milton --view --date=02/25/2009
|
31
|
+
|
32
|
+
== REQUIREMENTS:
|
33
|
+
|
34
|
+
* ADP ezLaborManager client name, username and password
|
35
|
+
|
36
|
+
== INSTALL:
|
37
|
+
|
38
|
+
* sudo gem install milton
|
39
|
+
|
40
|
+
== LICENSE:
|
41
|
+
|
42
|
+
(The MIT License)
|
43
|
+
|
44
|
+
Copyright (c) 2009 Aaron Patterson, Eric Hodel
|
45
|
+
|
46
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
47
|
+
a copy of this software and associated documentation files (the
|
48
|
+
'Software'), to deal in the Software without restriction, including
|
49
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
50
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
51
|
+
permit persons to whom the Software is furnished to do so, subject to
|
52
|
+
the following conditions:
|
53
|
+
|
54
|
+
The above copyright notice and this permission notice shall be
|
55
|
+
included in all copies or substantial portions of the Software.
|
56
|
+
|
57
|
+
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
|
58
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
59
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
60
|
+
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
61
|
+
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
62
|
+
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
63
|
+
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/Rakefile
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
# -*- ruby -*-
|
2
|
+
|
3
|
+
require 'rubygems'
|
4
|
+
require 'hoe'
|
5
|
+
require './lib/milton.rb'
|
6
|
+
|
7
|
+
Hoe.new('milton', Milton::VERSION) do |p|
|
8
|
+
p.developer('Aaron Patterson', 'aaronp@rubyforge.org')
|
9
|
+
p.extra_deps = [['mechanize', '>= 0.9.2']]
|
10
|
+
end
|
11
|
+
|
12
|
+
# vim: syntax=Ruby
|
data/bin/milton
ADDED
data/bin/recorder
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
#!/usr/bin/env ruby -w
|
2
|
+
|
3
|
+
require 'webrick'
|
4
|
+
require 'webrick/httpproxy'
|
5
|
+
require 'logger'
|
6
|
+
|
7
|
+
last_request_time = Time.now
|
8
|
+
human_requests = Logger.new('requests.log')
|
9
|
+
|
10
|
+
s = WEBrick::HTTPProxyServer.new(
|
11
|
+
:Logger => nil,
|
12
|
+
:Port => 8080,
|
13
|
+
:ProxyContentHandler => lambda { |req,res|
|
14
|
+
# If the last request time is ten seconds ago, let's say that a human
|
15
|
+
# made it
|
16
|
+
if Time.now - last_request_time >= 10
|
17
|
+
human_requests.debug("#{req}")
|
18
|
+
end
|
19
|
+
last_request_time = Time.now
|
20
|
+
}
|
21
|
+
)
|
22
|
+
|
23
|
+
Signal.trap('INT') do
|
24
|
+
s.shutdown
|
25
|
+
end
|
26
|
+
|
27
|
+
s.start
|
data/lib/milton.rb
ADDED
@@ -0,0 +1,293 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'mechanize'
|
3
|
+
require 'logger'
|
4
|
+
require 'cgi'
|
5
|
+
require 'date'
|
6
|
+
require 'optparse'
|
7
|
+
require 'optparse/date'
|
8
|
+
require 'yaml'
|
9
|
+
|
10
|
+
##
|
11
|
+
# Milton fills out timesheets for the ADP ezLaborManager.
|
12
|
+
|
13
|
+
class Milton
|
14
|
+
|
15
|
+
VERSION = '1.0.0'
|
16
|
+
|
17
|
+
def self.load_config config_file
|
18
|
+
unless File.exist? config_file then
|
19
|
+
open config_file, 'wb' do |f|
|
20
|
+
f.write YAML.dump({
|
21
|
+
'client_name' => 'Your client name',
|
22
|
+
'username' => 'Your username',
|
23
|
+
'password' => 'Your password',
|
24
|
+
})
|
25
|
+
end
|
26
|
+
|
27
|
+
raise "Please fill out #{config_file}. We've created a template there for you to edit."
|
28
|
+
end
|
29
|
+
|
30
|
+
YAML.load_file config_file
|
31
|
+
end
|
32
|
+
|
33
|
+
def self.parse_args argv
|
34
|
+
options = {
|
35
|
+
'date' => nil,
|
36
|
+
'view' => false
|
37
|
+
}
|
38
|
+
|
39
|
+
opts = OptionParser.new do |opt|
|
40
|
+
opt.program_name = File.basename $0
|
41
|
+
opt.version = Milton::VERSION
|
42
|
+
opt.release = nil
|
43
|
+
opt.banner = <<-EOF
|
44
|
+
Usage: #{opt.program_name} [options]
|
45
|
+
|
46
|
+
Milton fills out your ADP timesheet for you. By default it fills it out for
|
47
|
+
the current week with eight hours/day.
|
48
|
+
EOF
|
49
|
+
|
50
|
+
opt.separator nil
|
51
|
+
|
52
|
+
opt.on('--view',
|
53
|
+
'Only view your current timesheet') do |value|
|
54
|
+
options['view'] = value
|
55
|
+
end
|
56
|
+
|
57
|
+
opt.on('--date=DATE', Date,
|
58
|
+
'Select week by day') do |value|
|
59
|
+
options['date'] = value
|
60
|
+
end
|
61
|
+
|
62
|
+
opt.on('--month [DATE]', Date, 'Select month by day') do |value|
|
63
|
+
options['view'] = true # For your safety, you can only view months
|
64
|
+
options['month'] = value || Date.today
|
65
|
+
end
|
66
|
+
|
67
|
+
opt.on('--fuck-the-man',
|
68
|
+
'Do not include lunch in your timesheet') do |value|
|
69
|
+
options['rows_per_day'] = 1
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
opts.parse! argv
|
74
|
+
|
75
|
+
options
|
76
|
+
end
|
77
|
+
|
78
|
+
def self.run argv = ARGV
|
79
|
+
config_file = File.join Gem.user_home, '.milton'
|
80
|
+
|
81
|
+
options = parse_args argv
|
82
|
+
|
83
|
+
config = load_config config_file
|
84
|
+
|
85
|
+
options.merge! config
|
86
|
+
|
87
|
+
new.run options
|
88
|
+
end
|
89
|
+
|
90
|
+
def initialize &block
|
91
|
+
@agent = WWW::Mechanize.new
|
92
|
+
@page = nil
|
93
|
+
@username = nil
|
94
|
+
yield self if block_given?
|
95
|
+
end
|
96
|
+
|
97
|
+
##
|
98
|
+
# Sets the client name +name+
|
99
|
+
|
100
|
+
def client_name= name
|
101
|
+
page = @agent.get('http://workforceportal.elabor.com/ezLaborManagerNetRedirect/clientlogin.aspx')
|
102
|
+
@page = page.form('ClientLoginForm') { |form|
|
103
|
+
form.txtClientName = name
|
104
|
+
form.hdnTimeZone = 'Pacific Standard Time'
|
105
|
+
form['__EVENTTARGET'] = 'btnSubmit'
|
106
|
+
}.submit
|
107
|
+
@page = @page.form_with(:action => /ezlmportaldc2.adp.com/).submit
|
108
|
+
@page = @page.form_with(:action => /adp\.com/).submit
|
109
|
+
end
|
110
|
+
|
111
|
+
##
|
112
|
+
# Logs in +username+ with +password+
|
113
|
+
|
114
|
+
def login username, password
|
115
|
+
@username = username
|
116
|
+
|
117
|
+
@page = @page.form('Login') { |form|
|
118
|
+
form['txtUserID'] = username
|
119
|
+
form['txtPassword'] = password
|
120
|
+
form['__EVENTTARGET'] = 'btnLogin'
|
121
|
+
}.submit
|
122
|
+
change_password if @page.body =~ /Old Password/
|
123
|
+
@page = @page.link_with(:text => 'Time Sheet').click
|
124
|
+
end
|
125
|
+
|
126
|
+
##
|
127
|
+
# Selects the current week's timesheet
|
128
|
+
|
129
|
+
def select_current_week
|
130
|
+
select_week_of Date.today
|
131
|
+
end
|
132
|
+
|
133
|
+
def rows_per_day= rows = 2
|
134
|
+
@rows_per_day = rows
|
135
|
+
@page = @page.form('Form1') { |form|
|
136
|
+
form['__EVENTTARGET'] = 'SETNOOFROWS'
|
137
|
+
form['__EVENTARGUMENT'] = rows.to_s
|
138
|
+
form['__PageDirty'] = 'False'
|
139
|
+
}.submit
|
140
|
+
end
|
141
|
+
|
142
|
+
def run config
|
143
|
+
self.client_name = config['client_name']
|
144
|
+
login config['username'], config['password']
|
145
|
+
|
146
|
+
date = config['date']
|
147
|
+
month = config['month']
|
148
|
+
|
149
|
+
if date then
|
150
|
+
select_week_of date
|
151
|
+
elsif month
|
152
|
+
select_month_of month
|
153
|
+
else
|
154
|
+
select_current_week
|
155
|
+
end
|
156
|
+
|
157
|
+
unless config['view'] then
|
158
|
+
self.rows_per_day = config['rows_per_day'] || 2
|
159
|
+
fill_timesheet
|
160
|
+
end
|
161
|
+
|
162
|
+
extract_timesheet
|
163
|
+
end
|
164
|
+
|
165
|
+
##
|
166
|
+
# Fills in timesheet rows that don't already have data
|
167
|
+
|
168
|
+
def fill_timesheet
|
169
|
+
rows = []
|
170
|
+
last_date = nil
|
171
|
+
|
172
|
+
parse_timesheet.each do |data|
|
173
|
+
next if data[0].to_i > 0
|
174
|
+
|
175
|
+
department = data[6]
|
176
|
+
employee_id = data[7]
|
177
|
+
date = Date.parse(CGI.unescape(data[1])).strftime('%m/%d/%Y')
|
178
|
+
|
179
|
+
start, finish = starting_and_ending_timestamp(date, last_date)
|
180
|
+
|
181
|
+
rows << ['0','','False','True','False','False','False',
|
182
|
+
"#{date} 12:00:00 AM",
|
183
|
+
start,'',
|
184
|
+
finish,
|
185
|
+
'8','',
|
186
|
+
department,
|
187
|
+
employee_id,
|
188
|
+
'','','','','','','','','','','','','','','','','','','','','EDIT','','','','','2','','0','False']
|
189
|
+
|
190
|
+
# This reset is for the timestamp calculations.
|
191
|
+
last_date = date
|
192
|
+
end
|
193
|
+
|
194
|
+
@page = @page.form('Form1') { |form|
|
195
|
+
## FIXME: Fill out this form
|
196
|
+
form['hdnRETURNDATA'] = rows.map { |row|
|
197
|
+
row.map { |value|
|
198
|
+
CGI.escape(value)
|
199
|
+
}.join('~~')
|
200
|
+
}.join('~||~')
|
201
|
+
|
202
|
+
form['__EVENTTARGET'] = 'TG:btnSubmitTop'
|
203
|
+
form['__PageDirty'] = 'True'
|
204
|
+
}.submit
|
205
|
+
end
|
206
|
+
|
207
|
+
##
|
208
|
+
# Prints out your timesheet for the selected time frame
|
209
|
+
|
210
|
+
def extract_timesheet
|
211
|
+
timesheet = parse_timesheet
|
212
|
+
|
213
|
+
department = timesheet.first[6]
|
214
|
+
employee_id = timesheet.first[7]
|
215
|
+
|
216
|
+
puts "Employee #{@username} id #{employee_id}, department #{department}"
|
217
|
+
|
218
|
+
puts "-" * 80
|
219
|
+
|
220
|
+
timesheet.each do |row|
|
221
|
+
if row[0] == '0' then
|
222
|
+
puts "#{row[2]} no time entered"
|
223
|
+
else
|
224
|
+
puts "#{row[2]} #{row[3]} to #{row[4]} for %2s hours" % row[5]
|
225
|
+
end
|
226
|
+
end
|
227
|
+
end
|
228
|
+
|
229
|
+
##
|
230
|
+
# Selects the timesheet for the week containing +date+
|
231
|
+
|
232
|
+
def select_week_of date
|
233
|
+
monday = date - date.wday + 1
|
234
|
+
friday = monday + 4
|
235
|
+
select_range(monday, friday)
|
236
|
+
end
|
237
|
+
|
238
|
+
def select_month_of date
|
239
|
+
first = date - date.mday + 1
|
240
|
+
last = Date.new(first.year, first.month, -1)
|
241
|
+
select_range(first, last)
|
242
|
+
end
|
243
|
+
|
244
|
+
private
|
245
|
+
|
246
|
+
def change_password
|
247
|
+
puts "Your password needs to be updated. :-("
|
248
|
+
puts "\t#{@page.uri}"
|
249
|
+
exit
|
250
|
+
end
|
251
|
+
|
252
|
+
def select_range start, finish
|
253
|
+
@page = @page.form('Form1') { |form|
|
254
|
+
form['__EVENTTARGET'] = 'ctrlDtRangeSelector'
|
255
|
+
form['ctrlDtRangeSelector:SelectionItem'] = '3' # Set to this week
|
256
|
+
form['ctrlDtRangeSelector:BeginDate'] = start.strftime('%m/%d/%Y')
|
257
|
+
form['ctrlDtRangeSelector:EndDate'] = finish.strftime('%m/%d/%Y')
|
258
|
+
form['__PageDirty'] = 'False'
|
259
|
+
}.submit
|
260
|
+
end
|
261
|
+
|
262
|
+
##
|
263
|
+
# Returns an array of arrays containing: row id, day start time, date, start
|
264
|
+
# time, end time, hours, department, employee id. All values are strings.
|
265
|
+
|
266
|
+
def parse_timesheet
|
267
|
+
@page.body.scan(/TCMS.oTD.push\((\[.*\])\)/).map do |match|
|
268
|
+
match[0].gsub(/"/, '').split(',').map { |x|
|
269
|
+
CGI.unescape(x.strip).delete('[]')
|
270
|
+
}.values_at(0, 7, 8, 9, 11, 12, 14, 15)
|
271
|
+
end
|
272
|
+
end
|
273
|
+
|
274
|
+
##
|
275
|
+
# Returns the starting and ending EZLabor-style timestamps for the
|
276
|
+
# current date row in the timesheet.
|
277
|
+
def starting_and_ending_timestamp(date, last_date)
|
278
|
+
if @rows_per_day == 2
|
279
|
+
if last_date == date
|
280
|
+
start_timestamp = "#{date} 08:30 AM"
|
281
|
+
end_timestamp = '12:00 PM'
|
282
|
+
else
|
283
|
+
start_timestamp = "#{date} 12:30 PM"
|
284
|
+
end_timestamp = '05:00 PM'
|
285
|
+
end
|
286
|
+
else
|
287
|
+
start_timestamp = "#{date} 08:30 AM"
|
288
|
+
end_timestamp = '04:30 PM'
|
289
|
+
end
|
290
|
+
return start_timestamp, end_timestamp
|
291
|
+
end
|
292
|
+
end
|
293
|
+
|
data/test/test_milton.rb
ADDED
File without changes
|
metadata
ADDED
@@ -0,0 +1,86 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: milton
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Aaron Patterson
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2009-06-04 00:00:00 -07:00
|
13
|
+
default_executable:
|
14
|
+
dependencies:
|
15
|
+
- !ruby/object:Gem::Dependency
|
16
|
+
name: mechanize
|
17
|
+
type: :runtime
|
18
|
+
version_requirement:
|
19
|
+
version_requirements: !ruby/object:Gem::Requirement
|
20
|
+
requirements:
|
21
|
+
- - ">="
|
22
|
+
- !ruby/object:Gem::Version
|
23
|
+
version: 0.9.2
|
24
|
+
version:
|
25
|
+
- !ruby/object:Gem::Dependency
|
26
|
+
name: hoe
|
27
|
+
type: :development
|
28
|
+
version_requirement:
|
29
|
+
version_requirements: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: 1.12.2
|
34
|
+
version:
|
35
|
+
description: Milton fills out your ADP ezLaborManager timesheet
|
36
|
+
email:
|
37
|
+
- aaronp@rubyforge.org
|
38
|
+
executables:
|
39
|
+
- milton
|
40
|
+
- recorder
|
41
|
+
extensions: []
|
42
|
+
|
43
|
+
extra_rdoc_files:
|
44
|
+
- History.txt
|
45
|
+
- Manifest.txt
|
46
|
+
- README.txt
|
47
|
+
files:
|
48
|
+
- History.txt
|
49
|
+
- Manifest.txt
|
50
|
+
- README.txt
|
51
|
+
- Rakefile
|
52
|
+
- bin/milton
|
53
|
+
- bin/recorder
|
54
|
+
- lib/milton.rb
|
55
|
+
- test/test_milton.rb
|
56
|
+
has_rdoc: true
|
57
|
+
homepage: http://seattlerb.rubyforge.org/milton
|
58
|
+
licenses: []
|
59
|
+
|
60
|
+
post_install_message:
|
61
|
+
rdoc_options:
|
62
|
+
- --main
|
63
|
+
- README.txt
|
64
|
+
require_paths:
|
65
|
+
- lib
|
66
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
67
|
+
requirements:
|
68
|
+
- - ">="
|
69
|
+
- !ruby/object:Gem::Version
|
70
|
+
version: "0"
|
71
|
+
version:
|
72
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
73
|
+
requirements:
|
74
|
+
- - ">="
|
75
|
+
- !ruby/object:Gem::Version
|
76
|
+
version: "0"
|
77
|
+
version:
|
78
|
+
requirements: []
|
79
|
+
|
80
|
+
rubyforge_project: milton
|
81
|
+
rubygems_version: 1.3.3
|
82
|
+
signing_key:
|
83
|
+
specification_version: 3
|
84
|
+
summary: Milton fills out your ADP ezLaborManager timesheet
|
85
|
+
test_files:
|
86
|
+
- test/test_milton.rb
|