achoo 0.3
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 +46 -0
- data/COPYING +339 -0
- data/README.rdoc +100 -0
- data/Rakefile +64 -0
- data/bin/achoo +48 -0
- data/bin/awake +31 -0
- data/bin/ical +17 -0
- data/bin/vcs_commits +31 -0
- data/lib/achoo.rb +140 -0
- data/lib/achoo/awake.rb +130 -0
- data/lib/achoo/binary.rb +7 -0
- data/lib/achoo/binary/cstruct.rb +60 -0
- data/lib/achoo/binary/utmp_record.rb +59 -0
- data/lib/achoo/form.rb +18 -0
- data/lib/achoo/hour_administration_form.rb +131 -0
- data/lib/achoo/hour_registration_form.rb +227 -0
- data/lib/achoo/hour_registration_form_ranged.rb +45 -0
- data/lib/achoo/ical.rb +58 -0
- data/lib/achoo/lock_month_form.rb +40 -0
- data/lib/achoo/open_timespan.rb +13 -0
- data/lib/achoo/rc_loader.rb +55 -0
- data/lib/achoo/system.rb +6 -0
- data/lib/achoo/system/pm_suspend.rb +30 -0
- data/lib/achoo/system/wtmp.rb +18 -0
- data/lib/achoo/term.rb +76 -0
- data/lib/achoo/term/menu.rb +55 -0
- data/lib/achoo/term/table.rb +101 -0
- data/lib/achoo/timespan.rb +119 -0
- data/lib/achoo/ui.rb +10 -0
- data/lib/achoo/ui/commands.rb +51 -0
- data/lib/achoo/ui/common.rb +15 -0
- data/lib/achoo/ui/date_chooser.rb +75 -0
- data/lib/achoo/ui/date_choosers.rb +21 -0
- data/lib/achoo/ui/exception_handling.rb +20 -0
- data/lib/achoo/ui/month_chooser.rb +27 -0
- data/lib/achoo/ui/optionally_ranged_date_chooser.rb +33 -0
- data/lib/achoo/ui/register_hours.rb +137 -0
- data/lib/achoo/vcs.rb +43 -0
- data/lib/achoo/vcs/git.rb +20 -0
- data/lib/achoo/vcs/subversion.rb +32 -0
- metadata +116 -0
data/Rakefile
ADDED
@@ -0,0 +1,64 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require 'rake/gempackagetask'
|
4
|
+
require 'rake/rdoctask'
|
5
|
+
require 'rake/testtask'
|
6
|
+
require 'rubygems'
|
7
|
+
|
8
|
+
load File.dirname(__FILE__) + '/achoo.gemspec'
|
9
|
+
|
10
|
+
task :default => ['test:unit']
|
11
|
+
|
12
|
+
namespace 'build' do
|
13
|
+
|
14
|
+
Rake::GemPackageTask.new(Spec) do |pkg|
|
15
|
+
pkg.need_tar = true
|
16
|
+
end
|
17
|
+
|
18
|
+
end
|
19
|
+
|
20
|
+
Rake::TestTask.new do |t|
|
21
|
+
t.libs << "test/lib"
|
22
|
+
t.test_files = FileList['test/**/test*.rb']
|
23
|
+
#t.verbose = true
|
24
|
+
end
|
25
|
+
|
26
|
+
namespace 'test' do
|
27
|
+
|
28
|
+
Rake::TestTask.new do |t|
|
29
|
+
t.name = :unit
|
30
|
+
t.libs << "test/lib"
|
31
|
+
t.test_files = FileList['test/unit/test*.rb']
|
32
|
+
end
|
33
|
+
|
34
|
+
Rake::TestTask.new do |t|
|
35
|
+
t.name = :acceptance
|
36
|
+
t.libs << "test/lib"
|
37
|
+
t.test_files = FileList['test/acceptance/test*.rb']
|
38
|
+
end
|
39
|
+
|
40
|
+
desc 'Measures test coverage'
|
41
|
+
task :coverage do
|
42
|
+
rm_f('coverage')
|
43
|
+
system("rcov -T -Ilib test/unit/test_*.rb")
|
44
|
+
end
|
45
|
+
|
46
|
+
end
|
47
|
+
|
48
|
+
namespace 'doc' do
|
49
|
+
|
50
|
+
Rake::RDocTask.new do |rd|
|
51
|
+
rd.title = 'Achoo --- The Achievo CLI'
|
52
|
+
rd.main = "README.rdoc"
|
53
|
+
rd.rdoc_files.include("README.rdoc", "lib/**/*.rb")
|
54
|
+
rd.template = `allison --path`.chop + '.rb'
|
55
|
+
rd.options << '--line-numbers' << '--inline-source'
|
56
|
+
rd.rdoc_dir = 'doc'
|
57
|
+
end
|
58
|
+
|
59
|
+
end
|
60
|
+
|
61
|
+
desc 'Install development dependencies'
|
62
|
+
task :setup do
|
63
|
+
system 'gem install shoulda rack thin redgreen allison'
|
64
|
+
end
|
data/bin/achoo
ADDED
@@ -0,0 +1,48 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'achoo'
|
4
|
+
require 'achoo/rc_loader'
|
5
|
+
require 'optparse'
|
6
|
+
|
7
|
+
include Achoo::RCLoader
|
8
|
+
|
9
|
+
def main
|
10
|
+
trap("INT") { exit }
|
11
|
+
|
12
|
+
abort "UTF-8 terminal required" unless utf8?
|
13
|
+
|
14
|
+
options = {
|
15
|
+
:log => false,
|
16
|
+
:rc_file => "#{ENV['HOME']}/.achoo",
|
17
|
+
}.merge(parse_args)
|
18
|
+
|
19
|
+
load_rc(options[:rc_file])
|
20
|
+
|
21
|
+
achoo = Achoo.new(options[:log])
|
22
|
+
achoo.start
|
23
|
+
end
|
24
|
+
|
25
|
+
def parse_args
|
26
|
+
options = {}
|
27
|
+
|
28
|
+
optparse = OptionParser.new do|opts|
|
29
|
+
opts.banner = "Usage: $0 [options]"
|
30
|
+
|
31
|
+
opts.on( '-l', '--log', 'Write http communication to achoo_http.log' ) do
|
32
|
+
options[:log] = true
|
33
|
+
end
|
34
|
+
|
35
|
+
opts.on('--rcfile FILE', 'FIX' ) do |file|
|
36
|
+
options[:rc_file] = file
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
optparse.parse!
|
41
|
+
options
|
42
|
+
end
|
43
|
+
|
44
|
+
def utf8?
|
45
|
+
`locale charmap`.chop == 'UTF-8'
|
46
|
+
end
|
47
|
+
|
48
|
+
main
|
data/bin/awake
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
#! /usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'achoo/awake'
|
4
|
+
require 'date'
|
5
|
+
require 'optparse'
|
6
|
+
|
7
|
+
def main
|
8
|
+
awake = Achoo::Awake.new
|
9
|
+
|
10
|
+
options = {
|
11
|
+
:all => false,
|
12
|
+
}
|
13
|
+
|
14
|
+
optparse = OptionParser.new do|opts|
|
15
|
+
opts.banner = "Usage: $0 [options]"
|
16
|
+
|
17
|
+
opts.on( '-a', '--all', 'FIX' ) do
|
18
|
+
options[:all] = true
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
optparse.parse!
|
23
|
+
|
24
|
+
if options[:all]
|
25
|
+
awake.all
|
26
|
+
else
|
27
|
+
awake.at(Date.today)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
main
|
data/bin/ical
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
#! /usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'achoo/ical'
|
4
|
+
require 'achoo/rc_loader'
|
5
|
+
require 'optparse'
|
6
|
+
|
7
|
+
include Achoo::RCLoader
|
8
|
+
|
9
|
+
def main
|
10
|
+
load_rc
|
11
|
+
|
12
|
+
RC[:ical].each do |config|
|
13
|
+
Achoo::ICal.from_http_request(config).print_events(Date.parse(ARGV[0]))
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
main
|
data/bin/vcs_commits
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
#! /usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'achoo/rc_loader'
|
4
|
+
require 'achoo/vcs'
|
5
|
+
require 'date'
|
6
|
+
require 'optparse'
|
7
|
+
|
8
|
+
include Achoo::RCLoader
|
9
|
+
|
10
|
+
def main
|
11
|
+
load_rc
|
12
|
+
|
13
|
+
options = {
|
14
|
+
:date => Date.today,
|
15
|
+
}
|
16
|
+
|
17
|
+
optparse = OptionParser.new do|opts|
|
18
|
+
opts.banner = "Usage: $0 [options]"
|
19
|
+
|
20
|
+
opts.on( '-d', '--date [DATE]', 'FIX' ) do |date|
|
21
|
+
options[:date] = Date.parse(date)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
optparse.parse!
|
26
|
+
|
27
|
+
Achoo::VCS.print_logs_for(options[:date], RC[:vcs_dirs])
|
28
|
+
|
29
|
+
end
|
30
|
+
|
31
|
+
main
|
data/lib/achoo.rb
ADDED
@@ -0,0 +1,140 @@
|
|
1
|
+
class Achoo; end
|
2
|
+
|
3
|
+
require 'achoo/term'
|
4
|
+
require 'achoo/ui'
|
5
|
+
require 'logger'
|
6
|
+
require 'mechanize'
|
7
|
+
|
8
|
+
|
9
|
+
|
10
|
+
class Achoo
|
11
|
+
|
12
|
+
include Achoo::UI::Commands
|
13
|
+
include Achoo::UI::ExceptionHandling
|
14
|
+
include Achoo::UI::RegisterHours
|
15
|
+
|
16
|
+
def initialize(log=false)
|
17
|
+
@agent = Mechanize.new
|
18
|
+
if log
|
19
|
+
@agent.log = Logger.new("achoo_http.log")
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def start
|
24
|
+
begin
|
25
|
+
print_welcome
|
26
|
+
login
|
27
|
+
scrape_urls
|
28
|
+
command_loop
|
29
|
+
rescue SystemExit => e
|
30
|
+
raise
|
31
|
+
rescue Exception => e
|
32
|
+
handle_fatal_exception("Something bad happened. Shutting down.", e)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
|
37
|
+
def print_welcome
|
38
|
+
puts Term.shadowbox("Welcome to Achoo!")
|
39
|
+
end
|
40
|
+
|
41
|
+
|
42
|
+
def command_loop
|
43
|
+
while true
|
44
|
+
begin
|
45
|
+
trap("INT", "DEFAULT");
|
46
|
+
answer = Term.choose('[1]',
|
47
|
+
["Register hours",
|
48
|
+
"Show flexitime balance",
|
49
|
+
"Day hour report",
|
50
|
+
"Week hour report",
|
51
|
+
"Holiday balance",
|
52
|
+
"Lock month",
|
53
|
+
],
|
54
|
+
"Exit",
|
55
|
+
['q', 'Q', ''])
|
56
|
+
dispatch(answer)
|
57
|
+
rescue Interrupt
|
58
|
+
puts # Add a new line in case we are prompting
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
|
64
|
+
def dispatch(command)
|
65
|
+
case command
|
66
|
+
when '0', 'q', 'Q'
|
67
|
+
exit
|
68
|
+
when '1', ''
|
69
|
+
register_hours(@agent)
|
70
|
+
when '2'
|
71
|
+
show_flexi_time(@agent)
|
72
|
+
when '3'
|
73
|
+
show_registered_hours_for_day(@agent)
|
74
|
+
when '4'
|
75
|
+
show_registered_hours_for_week(@agent)
|
76
|
+
when '5'
|
77
|
+
show_holiday_report(@agent)
|
78
|
+
when '6'
|
79
|
+
lock_month(@agent)
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
|
84
|
+
def scrape_urls
|
85
|
+
page = @agent.get(@agent.current_page.frames.find {|f| f.name == 'menu'}.href)
|
86
|
+
menu_links = page.search('a.menuItemLevel2')
|
87
|
+
|
88
|
+
RC[:hour_registration_url] = menu_link_to_url(menu_links, 'Time Registration')
|
89
|
+
RC[:lock_months_url] = menu_link_to_url(menu_links, 'Lock months')
|
90
|
+
RC[:holiday_report_url] = menu_link_to_url(menu_links, 'Holiday report')
|
91
|
+
RC[:hour_admin_url] = RC[:hour_registration_url]
|
92
|
+
end
|
93
|
+
|
94
|
+
def menu_link_to_url(menu_links, text)
|
95
|
+
a_tag = menu_links.find {|a|
|
96
|
+
a.text.strip == text
|
97
|
+
}
|
98
|
+
|
99
|
+
if a_tag.nil?
|
100
|
+
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.")
|
101
|
+
end
|
102
|
+
|
103
|
+
url = a_tag.attribute('onclick').value.match(/window\.open\('([^']+)/)[1]
|
104
|
+
return "#{RC[:url]}#{url}"
|
105
|
+
end
|
106
|
+
|
107
|
+
|
108
|
+
def login
|
109
|
+
load_cookies
|
110
|
+
|
111
|
+
puts "Fetching data ..."
|
112
|
+
page = @agent.get(RC[:url])
|
113
|
+
|
114
|
+
return if page.forms.empty?
|
115
|
+
|
116
|
+
puts "Logging in ..."
|
117
|
+
|
118
|
+
form = page.forms.first
|
119
|
+
form.auth_user = RC[:user]
|
120
|
+
form.auth_pw = RC[:password]
|
121
|
+
page = @agent.submit(form, form.buttons.first)
|
122
|
+
|
123
|
+
if page.body.match(/Username and\/or password are incorrect. Please try again./)
|
124
|
+
raise "Username and/or password are incorrect."
|
125
|
+
end
|
126
|
+
|
127
|
+
@agent.cookie_jar.save_as("#{ENV['HOME']}/.achoo_cookies.yml")
|
128
|
+
end
|
129
|
+
|
130
|
+
|
131
|
+
def load_cookies
|
132
|
+
cookies_file = "#{ENV['HOME']}/.achoo_cookies.yml"
|
133
|
+
if FileTest.exists? cookies_file
|
134
|
+
@agent.cookie_jar.load(cookies_file)
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
|
139
|
+
end
|
140
|
+
|
data/lib/achoo/awake.rb
ADDED
@@ -0,0 +1,130 @@
|
|
1
|
+
require 'achoo/open_timespan'
|
2
|
+
require 'achoo/timespan'
|
3
|
+
require 'achoo/system'
|
4
|
+
|
5
|
+
class Achoo; end
|
6
|
+
|
7
|
+
class Achoo::Awake
|
8
|
+
|
9
|
+
def initialize
|
10
|
+
log = array_merge(wtmp, suspend) {|a, b| a[0] >= b[0] }
|
11
|
+
log.unshift([Time.now, :now])
|
12
|
+
@sessions = sessions(log)
|
13
|
+
end
|
14
|
+
|
15
|
+
def at(date)
|
16
|
+
span = Achoo::Timespan.new(date, date+1)
|
17
|
+
@sessions.each do |s|
|
18
|
+
print_session(s, span) if s[0].overlaps?(span)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def all
|
23
|
+
@sessions.each do |s|
|
24
|
+
print_session(s)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
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
|
35
|
+
end
|
36
|
+
|
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[1] == :boot
|
41
|
+
|
42
|
+
session = []
|
43
|
+
case g.first[1]
|
44
|
+
when :now, :halt, :suspend
|
45
|
+
# We know the end of the session
|
46
|
+
session << Achoo::Timespan.new(g.last[0], g.first[0])
|
47
|
+
else # :awake, :boot
|
48
|
+
# We don't know the end of the session
|
49
|
+
session << Achoo::OpenTimespan.new(g.last[0], g.first[0])
|
50
|
+
g.unshift([-1, :crash])
|
51
|
+
end
|
52
|
+
|
53
|
+
raise "Session must consist of even number of events. Found #{g.length}" unless g.length.even?
|
54
|
+
|
55
|
+
|
56
|
+
session << []
|
57
|
+
unless g.length == 2
|
58
|
+
i = 0
|
59
|
+
while i < g.length-1
|
60
|
+
klass = g[i][1] == :crash ? Achoo::OpenTimespan : Achoo::Timespan
|
61
|
+
session[1] << klass.new(g[i+1][0], g[i][0])
|
62
|
+
i += 2
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
sessions << session
|
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
|
78
|
+
end
|
79
|
+
end
|
80
|
+
array.concat(a1).concat(a2)
|
81
|
+
array
|
82
|
+
end
|
83
|
+
|
84
|
+
def suspend
|
85
|
+
log = Achoo::System::PMSuspend.new.reverse
|
86
|
+
new_log = []
|
87
|
+
log.each do |entry|
|
88
|
+
new_log << [entry.time, entry.action == 'Awake.' ? :awake : :suspend]
|
89
|
+
end
|
90
|
+
new_log
|
91
|
+
end
|
92
|
+
|
93
|
+
def wtmp
|
94
|
+
log = Achoo::System::Wtmp.new.reverse
|
95
|
+
new_log = []
|
96
|
+
log.each do |entry|
|
97
|
+
if entry.boot_event?
|
98
|
+
new_log << [entry.time, :boot]
|
99
|
+
elsif entry.halt_event?
|
100
|
+
new_log << [entry.time, :halt]
|
101
|
+
end
|
102
|
+
end
|
103
|
+
new_log
|
104
|
+
end
|
105
|
+
|
106
|
+
def group(log)
|
107
|
+
grouped_log = []
|
108
|
+
group = nil
|
109
|
+
log.each do |i|
|
110
|
+
if i[1] == :halt || i[1] == :now
|
111
|
+
group = [i]
|
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
|
121
|
+
else
|
122
|
+
group << i
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
126
|
+
grouped_log
|
127
|
+
end
|
128
|
+
|
129
|
+
|
130
|
+
end
|