achoo 0.3 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/CHANGES +12 -0
- data/README.rdoc +26 -30
- data/Rakefile +3 -0
- data/bin/achoo +2 -2
- data/lib/achoo.rb +1 -139
- data/lib/achoo/achievo.rb +13 -0
- data/lib/achoo/achievo/form.rb +22 -0
- data/lib/achoo/achievo/hour_administration_form.rb +91 -0
- data/lib/achoo/achievo/hour_registration_form.rb +230 -0
- data/lib/achoo/achievo/hour_registration_form_ranged.rb +49 -0
- data/lib/achoo/achievo/lock_month_form.rb +44 -0
- data/lib/achoo/achievo/login_form.rb +27 -0
- data/lib/achoo/achievo/table.rb +30 -0
- data/lib/achoo/app.rb +153 -0
- data/lib/achoo/awake.rb +86 -100
- data/lib/achoo/extensions.rb +24 -0
- data/lib/achoo/ical.rb +47 -40
- data/lib/achoo/rc_loader.rb +42 -42
- data/lib/achoo/system.rb +8 -3
- data/lib/achoo/system/cstruct.rb +67 -0
- data/lib/achoo/system/log_entry.rb +24 -0
- data/lib/achoo/system/pm_suspend.rb +17 -20
- data/lib/achoo/system/utmp_record.rb +64 -0
- data/lib/achoo/system/wtmp.rb +14 -10
- data/lib/achoo/temporal.rb +8 -0
- data/lib/achoo/temporal/open_timespan.rb +16 -0
- data/lib/achoo/temporal/timespan.rb +122 -0
- data/lib/achoo/term.rb +58 -56
- data/lib/achoo/term/menu.rb +2 -2
- data/lib/achoo/term/table.rb +59 -60
- data/lib/achoo/ui.rb +13 -9
- data/lib/achoo/ui/commands.rb +60 -38
- data/lib/achoo/ui/common.rb +10 -7
- data/lib/achoo/ui/date_chooser.rb +69 -65
- data/lib/achoo/ui/date_choosers.rb +15 -14
- data/lib/achoo/ui/exception_handling.rb +14 -12
- data/lib/achoo/ui/month_chooser.rb +37 -24
- data/lib/achoo/ui/optionally_ranged_date_chooser.rb +29 -25
- data/lib/achoo/ui/register_hours.rb +116 -114
- data/lib/achoo/vcs.rb +32 -30
- data/lib/achoo/vcs/git.rb +18 -14
- data/lib/achoo/vcs/subversion.rb +25 -23
- metadata +30 -24
- data/lib/achoo/binary.rb +0 -7
- data/lib/achoo/binary/cstruct.rb +0 -60
- data/lib/achoo/binary/utmp_record.rb +0 -59
- data/lib/achoo/form.rb +0 -18
- data/lib/achoo/hour_administration_form.rb +0 -131
- data/lib/achoo/hour_registration_form.rb +0 -227
- data/lib/achoo/hour_registration_form_ranged.rb +0 -45
- data/lib/achoo/lock_month_form.rb +0 -40
- data/lib/achoo/open_timespan.rb +0 -13
- data/lib/achoo/timespan.rb +0 -119
@@ -0,0 +1,49 @@
|
|
1
|
+
require 'achoo/achievo'
|
2
|
+
|
3
|
+
module Achoo
|
4
|
+
module Achievo
|
5
|
+
class HourRegistrationFormRanged < HourRegistrationForm
|
6
|
+
|
7
|
+
def initialize(agent)
|
8
|
+
super
|
9
|
+
|
10
|
+
@page = @agent.get(atk_submit_to_url(@page.link_with(:text => 'Select range').href))
|
11
|
+
@form = @page.form('entryform')
|
12
|
+
end
|
13
|
+
|
14
|
+
def date=(date_range)
|
15
|
+
super(date_range[0])
|
16
|
+
|
17
|
+
to_day_field.value = date_range[1].strftime('%d')
|
18
|
+
to_month_field.value = date_range[1].strftime('%m')
|
19
|
+
to_year_field.value = date_range[1].year
|
20
|
+
end
|
21
|
+
|
22
|
+
def date
|
23
|
+
start = super
|
24
|
+
finish = Date.new(to_year_field.value.to_i, to_month_field.value.to_i,
|
25
|
+
to_day_field.value.to_i)
|
26
|
+
[start, finish]
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def to_day_field
|
32
|
+
@form.field_with(:name => 'todate[day]')
|
33
|
+
end
|
34
|
+
|
35
|
+
def to_month_field
|
36
|
+
@form.field_with(:name => 'todate[month]')
|
37
|
+
end
|
38
|
+
|
39
|
+
def to_year_field
|
40
|
+
@form.field_with(:name => 'todate[year]')
|
41
|
+
end
|
42
|
+
|
43
|
+
def date_to_s
|
44
|
+
date.map {|d| d.strftime("%Y-%m-%d")}.join(" -> ")
|
45
|
+
end
|
46
|
+
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
require 'achoo/achievo'
|
2
|
+
|
3
|
+
module Achoo
|
4
|
+
module Achievo
|
5
|
+
class LockMonthForm
|
6
|
+
|
7
|
+
def initialize(agent)
|
8
|
+
@agent = agent
|
9
|
+
end
|
10
|
+
|
11
|
+
def lock_month(period)
|
12
|
+
page = @agent.get(RC[:lock_months_url])
|
13
|
+
@form = page.form('entryform')
|
14
|
+
|
15
|
+
@form.period = period
|
16
|
+
unless user_select.nil?
|
17
|
+
user_select.options.each do |opt|
|
18
|
+
if opt.text.match(/\(#{RC[:user]}\)$/)
|
19
|
+
opt.select
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
25
|
+
|
26
|
+
def print_values
|
27
|
+
puts "Month: #{@form.period}"
|
28
|
+
puts " User: #{user_select.value}" unless user_select.nil?
|
29
|
+
|
30
|
+
end
|
31
|
+
|
32
|
+
def submit
|
33
|
+
@form.submit
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
def user_select
|
39
|
+
@form.field_with(:name => 'userid')
|
40
|
+
end
|
41
|
+
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
require 'achoo/achievo'
|
2
|
+
|
3
|
+
module Achoo
|
4
|
+
module Achievo
|
5
|
+
module LoginForm
|
6
|
+
|
7
|
+
def self.login(agent)
|
8
|
+
puts "Fetching data ..."
|
9
|
+
page = agent.get(RC[:url])
|
10
|
+
|
11
|
+
return if page.forms.empty? # already logged in
|
12
|
+
|
13
|
+
puts "Logging in ..."
|
14
|
+
|
15
|
+
form = page.forms.first
|
16
|
+
form.auth_user = RC[:user]
|
17
|
+
form.auth_pw = RC[:password]
|
18
|
+
page = agent.submit(form, form.buttons.first)
|
19
|
+
|
20
|
+
if page.body.match(/Username and\/or password are incorrect. Please try again./)
|
21
|
+
raise "Username and/or password are incorrect."
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
require 'achoo/achievo'
|
2
|
+
|
3
|
+
module Achoo
|
4
|
+
module Achievo
|
5
|
+
class Table < Array
|
6
|
+
|
7
|
+
def initialize(source_rows)
|
8
|
+
super()
|
9
|
+
source_rows.each do |tr|
|
10
|
+
cells = tr.css('td, th')
|
11
|
+
cells = cells.collect {|c| c.content.strip}
|
12
|
+
self << fix_empty_cells(cells)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def select_columns(&block)
|
17
|
+
columns = transpose
|
18
|
+
columns = columns.select &block
|
19
|
+
replace(columns.transpose)
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def fix_empty_cells(row)
|
25
|
+
row.collect {|c| c == "\302\240" ? ' ' : c} # UTF-8 NO-BREAK-SPACE
|
26
|
+
end
|
27
|
+
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
data/lib/achoo/app.rb
ADDED
@@ -0,0 +1,153 @@
|
|
1
|
+
require 'achoo'
|
2
|
+
require 'achoo/achievo'
|
3
|
+
require 'achoo/term'
|
4
|
+
require 'achoo/ui'
|
5
|
+
require 'logger'
|
6
|
+
require 'mechanize'
|
7
|
+
|
8
|
+
|
9
|
+
module Achoo
|
10
|
+
class App
|
11
|
+
|
12
|
+
include UI::Commands
|
13
|
+
include UI::ExceptionHandling
|
14
|
+
include UI::RegisterHours
|
15
|
+
|
16
|
+
|
17
|
+
def initialize(log=false)
|
18
|
+
@agent = Mechanize.new
|
19
|
+
if log
|
20
|
+
@agent.log = Logger.new("achoo_http.log")
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
|
25
|
+
def start
|
26
|
+
begin
|
27
|
+
print_welcome
|
28
|
+
warm_up_ical_cache
|
29
|
+
login
|
30
|
+
scrape_urls
|
31
|
+
command_loop
|
32
|
+
rescue SystemExit => e
|
33
|
+
raise
|
34
|
+
rescue Exception => e
|
35
|
+
handle_fatal_exception("Something bad happened. Shutting down.", e)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
|
43
|
+
def print_welcome
|
44
|
+
puts Term.shadowbox("Welcome to Achoo!")
|
45
|
+
end
|
46
|
+
|
47
|
+
|
48
|
+
def command_loop
|
49
|
+
while true
|
50
|
+
begin
|
51
|
+
trap("INT", "DEFAULT");
|
52
|
+
choices = ["Register hours",
|
53
|
+
"Show flexitime balance",
|
54
|
+
"Day hour report",
|
55
|
+
"Week hour report",
|
56
|
+
"Holiday balance",
|
57
|
+
"Lock month",
|
58
|
+
]
|
59
|
+
choices << "Time survey report" if RC[:reports]
|
60
|
+
answer = Term.choose('[1]',
|
61
|
+
choices,
|
62
|
+
"Exit",
|
63
|
+
['q', 'Q', ''])
|
64
|
+
dispatch(answer)
|
65
|
+
rescue Interrupt
|
66
|
+
puts # Add a new line in case we are prompting
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
|
72
|
+
def dispatch(command)
|
73
|
+
case command
|
74
|
+
when '0', 'q', 'Q'
|
75
|
+
exit
|
76
|
+
when '1', ''
|
77
|
+
register_hours(@agent)
|
78
|
+
when '2'
|
79
|
+
show_flexi_time(@agent)
|
80
|
+
when '3'
|
81
|
+
show_registered_hours_for_day(@agent)
|
82
|
+
when '4'
|
83
|
+
show_registered_hours_for_week(@agent)
|
84
|
+
when '5'
|
85
|
+
show_holiday_report(@agent)
|
86
|
+
when '6'
|
87
|
+
lock_month(@agent)
|
88
|
+
when '7'
|
89
|
+
view_report(@agent)
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
|
94
|
+
def scrape_urls
|
95
|
+
page = @agent.get(@agent.current_page.frames.find {|f| f.name == 'menu'}.href)
|
96
|
+
menu_links = page.search('a.menuItemLevel2')
|
97
|
+
|
98
|
+
RC[:hour_registration_url] = menu_link_to_url(menu_links, 'Time Registration')
|
99
|
+
RC[:lock_months_url] = menu_link_to_url(menu_links, 'Lock months')
|
100
|
+
RC[:holiday_report_url] = menu_link_to_url(menu_links, 'Holiday report')
|
101
|
+
RC[:hour_admin_url] = RC[:hour_registration_url]
|
102
|
+
end
|
103
|
+
|
104
|
+
|
105
|
+
def menu_link_to_url(menu_links, text)
|
106
|
+
a_tag = menu_links.find {|a|
|
107
|
+
a.text.strip == text
|
108
|
+
}
|
109
|
+
|
110
|
+
if a_tag.nil?
|
111
|
+
raise Exception.new("Could not find the '#{text}' link in Achievo.\nMake sure that language is 'English' and theme is 'no value' in Achievo's user preferences.\nYou must delete ~/.achoo_cookies.yml for these changes to take affect.")
|
112
|
+
end
|
113
|
+
|
114
|
+
url = a_tag.attribute('onclick').value.match(/window\.open\('([^']+)/)[1]
|
115
|
+
return "#{RC[:url]}#{url}"
|
116
|
+
end
|
117
|
+
|
118
|
+
|
119
|
+
def login
|
120
|
+
load_cookies
|
121
|
+
Achievo::LoginForm.login(@agent)
|
122
|
+
save_cookies
|
123
|
+
end
|
124
|
+
|
125
|
+
|
126
|
+
def load_cookies
|
127
|
+
cookies_file = "#{ENV['HOME']}/.achoo_cookies.yml"
|
128
|
+
if FileTest.exists? cookies_file
|
129
|
+
@agent.cookie_jar.load(cookies_file)
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
|
134
|
+
def save_cookies
|
135
|
+
@agent.cookie_jar.save_as("#{ENV['HOME']}/.achoo_cookies.yml")
|
136
|
+
end
|
137
|
+
|
138
|
+
|
139
|
+
def warm_up_ical_cache
|
140
|
+
Thread.new do
|
141
|
+
RC[:ical].each do |config|
|
142
|
+
begin
|
143
|
+
ICal.from_http_request(config)
|
144
|
+
rescue Exception => e
|
145
|
+
# Ignore, we are just doing this to populate the cache
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
data/lib/achoo/awake.rb
CHANGED
@@ -1,130 +1,116 @@
|
|
1
|
-
require 'achoo
|
2
|
-
require 'achoo/
|
1
|
+
require 'achoo'
|
2
|
+
require 'achoo/extensions'
|
3
3
|
require 'achoo/system'
|
4
|
+
require 'achoo/temporal'
|
4
5
|
|
5
|
-
|
6
|
+
module Achoo
|
7
|
+
class Awake
|
6
8
|
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
@sessions = sessions(log)
|
13
|
-
end
|
9
|
+
def initialize
|
10
|
+
log = wtmp.merge!(suspend)
|
11
|
+
log.unshift(System::LogEntry.new(Time.now, :now))
|
12
|
+
@sessions = sessions(log)
|
13
|
+
end
|
14
14
|
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
15
|
+
def at(date)
|
16
|
+
span = Temporal::Timespan.new(date, date+1)
|
17
|
+
@sessions.each do |s|
|
18
|
+
print_session(s, span) if s[0].overlaps?(span)
|
19
|
+
end
|
19
20
|
end
|
20
|
-
end
|
21
21
|
|
22
|
-
|
23
|
-
|
24
|
-
|
22
|
+
def all
|
23
|
+
@sessions.each do |s|
|
24
|
+
print_session(s)
|
25
|
+
end
|
25
26
|
end
|
26
|
-
end
|
27
27
|
|
28
|
-
|
28
|
+
private
|
29
29
|
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
30
|
+
def print_session(s, span=nil)
|
31
|
+
puts "Powered on: " << s[0].to_s
|
32
|
+
s[1].each do |t|
|
33
|
+
puts " Awake: " << t.to_s if span.nil? || t.overlaps?(span)
|
34
|
+
end
|
34
35
|
end
|
35
|
-
end
|
36
36
|
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
37
|
+
def sessions(log)
|
38
|
+
sessions = []
|
39
|
+
group(log).each do |g|
|
40
|
+
raise "Session doesn't begin with a boot event" unless g.last.event == :boot
|
41
|
+
|
42
|
+
session = []
|
43
|
+
case g.first.event
|
44
|
+
when :now, :halt, :suspend
|
45
|
+
# We know the end of the session
|
46
|
+
session << Temporal::Timespan.new(g.last.time, g.first.time)
|
47
|
+
else # :awake, :boot
|
48
|
+
# We don't know the end of the session
|
49
|
+
session << Temporal::OpenTimespan.new(g.last.time, g.first.time)
|
50
|
+
g.unshift(System::LogEntry.new(-1, :crash))
|
51
|
+
end
|
52
52
|
|
53
|
-
|
54
|
-
|
53
|
+
raise "Session must consist of even number of events. Found #{g.length}" unless g.length.even?
|
54
|
+
|
55
55
|
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
56
|
+
session << []
|
57
|
+
unless g.length == 2
|
58
|
+
i = 0
|
59
|
+
while i < g.length-1
|
60
|
+
klass = g[i].event == :crash ? Temporal::OpenTimespan : Temporal::Timespan
|
61
|
+
session[1] << klass.new(g[i+1].time, g[i].time)
|
62
|
+
i += 2
|
63
|
+
end
|
63
64
|
end
|
64
|
-
end
|
65
65
|
|
66
|
-
|
67
|
-
end
|
68
|
-
sessions
|
69
|
-
end
|
70
|
-
|
71
|
-
def array_merge(a1, a2)
|
72
|
-
array = []
|
73
|
-
until a1.empty? or a2.empty?
|
74
|
-
if yield(a1.first, a2.first)
|
75
|
-
array << a1.shift
|
76
|
-
else
|
77
|
-
array << a2.shift
|
66
|
+
sessions << session
|
78
67
|
end
|
68
|
+
sessions
|
79
69
|
end
|
80
|
-
array.concat(a1).concat(a2)
|
81
|
-
array
|
82
|
-
end
|
83
70
|
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
71
|
+
def suspend
|
72
|
+
log = System::PMSuspend.new.reverse
|
73
|
+
new_log = []
|
74
|
+
log.each do |entry|
|
75
|
+
new_log << System::LogEntry.new(entry.time, entry.event == 'Awake.' ? :awake : :suspend)
|
76
|
+
end
|
77
|
+
new_log
|
89
78
|
end
|
90
|
-
new_log
|
91
|
-
end
|
92
79
|
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
80
|
+
def wtmp
|
81
|
+
log = System::Wtmp.new.reverse
|
82
|
+
new_log = []
|
83
|
+
log.each do |entry|
|
84
|
+
if entry.boot_event?
|
85
|
+
new_log << System::LogEntry.new(entry.time, :boot)
|
86
|
+
elsif entry.halt_event?
|
87
|
+
new_log << System::LogEntry.new(entry.time, :halt)
|
88
|
+
end
|
101
89
|
end
|
90
|
+
new_log
|
102
91
|
end
|
103
|
-
new_log
|
104
|
-
end
|
105
92
|
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
else
|
113
|
-
if group.nil?
|
114
|
-
# Crashed
|
115
|
-
group = []
|
116
|
-
end
|
117
|
-
if i[1] == :boot
|
118
|
-
group << i
|
119
|
-
grouped_log << group
|
120
|
-
group = nil
|
93
|
+
def group(log)
|
94
|
+
grouped_log = []
|
95
|
+
group = nil
|
96
|
+
log.each do |i|
|
97
|
+
if i.event == :halt || i.event == :now
|
98
|
+
group = [i]
|
121
99
|
else
|
100
|
+
if group.nil?
|
101
|
+
# Crashed
|
102
|
+
group = []
|
103
|
+
end
|
122
104
|
group << i
|
123
|
-
|
105
|
+
if i.event == :boot
|
106
|
+
grouped_log << group
|
107
|
+
group = nil
|
108
|
+
end
|
109
|
+
end
|
124
110
|
end
|
111
|
+
grouped_log
|
125
112
|
end
|
126
|
-
grouped_log
|
127
|
-
end
|
128
113
|
|
129
114
|
|
115
|
+
end
|
130
116
|
end
|