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
data/lib/achoo/binary.rb
DELETED
data/lib/achoo/binary/cstruct.rb
DELETED
@@ -1,60 +0,0 @@
|
|
1
|
-
require 'achoo/binary'
|
2
|
-
|
3
|
-
class Achoo::Binary::CStruct
|
4
|
-
|
5
|
-
def initialize(bytes=nil)
|
6
|
-
@values = []
|
7
|
-
unpack(bytes) unless bytes.nil?
|
8
|
-
end
|
9
|
-
|
10
|
-
class << self
|
11
|
-
|
12
|
-
attr :template
|
13
|
-
|
14
|
-
def inherited(subclass)
|
15
|
-
subclass.instance_variable_set(:@template, '')
|
16
|
-
subclass.instance_variable_set(:@count, 0)
|
17
|
-
end
|
18
|
-
|
19
|
-
def char(name); add_type(name, :char, 'c', 0); end
|
20
|
-
def short(name); add_type(name, :short, 's', 0); end
|
21
|
-
def long(name); add_type(name, :long, 'l', 0); end
|
22
|
-
def quad(name); add_type(name, :quad, 'q', 0); end
|
23
|
-
|
24
|
-
def string(name, length); add_type(name, :string, 'A', '', length); end
|
25
|
-
|
26
|
-
def bin_size
|
27
|
-
@bin_size ||= template.split('').select {|c| c =~ /[[:alpha:]]/}.map do |c|
|
28
|
-
c == 'A' ? '' : 0
|
29
|
-
end.pack(template).length
|
30
|
-
end
|
31
|
-
|
32
|
-
private
|
33
|
-
|
34
|
-
def add_type(name, type, temp, zero, length=nil)
|
35
|
-
template << temp
|
36
|
-
template << length.to_s if type == :string
|
37
|
-
index = @count
|
38
|
-
@count += 1
|
39
|
-
|
40
|
-
send(:define_method, name) do
|
41
|
-
@values[index]
|
42
|
-
end
|
43
|
-
|
44
|
-
send(:define_method, "#{name}=") do |val|
|
45
|
-
@values[index] = val
|
46
|
-
end
|
47
|
-
end
|
48
|
-
|
49
|
-
end
|
50
|
-
|
51
|
-
def unpack(str)
|
52
|
-
@values = str.unpack(self.class.template)
|
53
|
-
end
|
54
|
-
|
55
|
-
def pack
|
56
|
-
t = self.class.template.tr('A', 'a')
|
57
|
-
@values.pack(t)
|
58
|
-
end
|
59
|
-
|
60
|
-
end
|
@@ -1,59 +0,0 @@
|
|
1
|
-
require 'achoo/binary'
|
2
|
-
|
3
|
-
class Achoo::Binary::UTMPRecord < Achoo::Binary::CStruct
|
4
|
-
long :record_type
|
5
|
-
long :process_id
|
6
|
-
string :device_name, 32
|
7
|
-
string :inittab_id, 4
|
8
|
-
string :username, 32
|
9
|
-
string :hostname, 256
|
10
|
-
short :termination_status
|
11
|
-
short :exit_status
|
12
|
-
long :session_id
|
13
|
-
long :seconds
|
14
|
-
long :milliseconds
|
15
|
-
long :ip_address1
|
16
|
-
long :ip_address2
|
17
|
-
long :ip_address3
|
18
|
-
long :ip_address4
|
19
|
-
string :unused, 20
|
20
|
-
|
21
|
-
|
22
|
-
TYPE_MAP = [:empty,
|
23
|
-
:run_lvl,
|
24
|
-
:boot,
|
25
|
-
:new_time,
|
26
|
-
:old_time,
|
27
|
-
:init,
|
28
|
-
:login,
|
29
|
-
:normal,
|
30
|
-
:term,
|
31
|
-
:account,
|
32
|
-
]
|
33
|
-
|
34
|
-
def time
|
35
|
-
return nil if seconds.nil?
|
36
|
-
@time ||= Time.at(seconds, milliseconds)
|
37
|
-
end
|
38
|
-
|
39
|
-
def record_type_symbol
|
40
|
-
TYPE_MAP[record_type]
|
41
|
-
end
|
42
|
-
|
43
|
-
def record_type_symbol=(sym)
|
44
|
-
@values[0] = (TYPE_MAP.find_index(sym))
|
45
|
-
end
|
46
|
-
|
47
|
-
def to_s
|
48
|
-
sprintf "%s %-7s %-8s %s", time.strftime('%F_%T'), record_type_symbol, username, device_name
|
49
|
-
end
|
50
|
-
|
51
|
-
def boot_event?
|
52
|
-
record_type_symbol == :boot
|
53
|
-
end
|
54
|
-
|
55
|
-
def halt_event?
|
56
|
-
record_type_symbol == :term && device_name == ':0'
|
57
|
-
end
|
58
|
-
|
59
|
-
end
|
data/lib/achoo/form.rb
DELETED
@@ -1,18 +0,0 @@
|
|
1
|
-
|
2
|
-
class Achoo; end
|
3
|
-
|
4
|
-
class Achoo::Form
|
5
|
-
|
6
|
-
def date=(date)
|
7
|
-
# Day and month must be prefixed with '0' if single
|
8
|
-
# digit. Date.day and Date.month doesn't do this. Use strftime
|
9
|
-
day_field.value = date.strftime('%d')
|
10
|
-
month_field.value = date.strftime('%m')
|
11
|
-
year_field.value = date.year
|
12
|
-
end
|
13
|
-
|
14
|
-
def date
|
15
|
-
Date.new(year_field.value.to_i, month_field.value.to_i,
|
16
|
-
day_field.value.to_i)
|
17
|
-
end
|
18
|
-
end
|
@@ -1,131 +0,0 @@
|
|
1
|
-
require 'achoo/form'
|
2
|
-
require 'achoo/term/table'
|
3
|
-
|
4
|
-
class Achoo::HourAdministrationForm < Achoo::Form
|
5
|
-
|
6
|
-
def initialize(agent)
|
7
|
-
@agent = agent
|
8
|
-
@page = nil
|
9
|
-
end
|
10
|
-
|
11
|
-
def show_registered_hours_for_day(date)
|
12
|
-
show_registered_hours(date, 'dayview', '#rl_1 tr')
|
13
|
-
end
|
14
|
-
|
15
|
-
def show_registered_hours_for_week(date)
|
16
|
-
show_registered_hours(date, 'weekview', '//form[@name="weekview"]/following::table/tr')
|
17
|
-
end
|
18
|
-
|
19
|
-
def flexi_time(date)
|
20
|
-
set_page_to_view_for_date('dayview', date)
|
21
|
-
|
22
|
-
@page.body.match(/Flexi time balance: (-?\d+:\d+)/)[1]
|
23
|
-
end
|
24
|
-
|
25
|
-
private
|
26
|
-
|
27
|
-
def show_registered_hours(date, view, query)
|
28
|
-
set_page_to_view_for_date(view, date)
|
29
|
-
|
30
|
-
columns = choose_source_columns(view, query)
|
31
|
-
source_rows = @page.search(query)
|
32
|
-
headers = extract_headers(source_rows, columns, view)
|
33
|
-
data_rows = extract_data_rows(source_rows, columns)
|
34
|
-
summaries = extract_summaries(source_rows, data_rows, columns)
|
35
|
-
|
36
|
-
Achoo::Term::Table.new(headers, data_rows, summaries).print
|
37
|
-
end
|
38
|
-
|
39
|
-
def set_page_to_view_for_date(view, date)
|
40
|
-
@page ||= @agent.get(RC[:hour_admin_url])
|
41
|
-
|
42
|
-
link = @page.link_with(:text => view.capitalize)
|
43
|
-
@form = @page.form(view)
|
44
|
-
unless link.nil?
|
45
|
-
puts "Fetching #{view} ..."
|
46
|
-
@page = link.click
|
47
|
-
@form = @page.form(view)
|
48
|
-
end
|
49
|
-
unless date == self.date
|
50
|
-
@page = get_page_for(date)
|
51
|
-
@form = @page.form(view)
|
52
|
-
end
|
53
|
-
|
54
|
-
@page
|
55
|
-
end
|
56
|
-
|
57
|
-
def choose_source_columns(view, query)
|
58
|
-
columns = nil
|
59
|
-
if view == 'dayview'
|
60
|
-
# Ignore 'Billing billed', 'Billing marked', and 'Billing total'
|
61
|
-
columns = [0,1,2,3,6,8,9]
|
62
|
-
# Achievo prepends an extra column dynamically if there are
|
63
|
-
# data rows.
|
64
|
-
unless @page.search(query + ' td').empty?
|
65
|
-
columns.collect! {|c| c + 1}
|
66
|
-
end
|
67
|
-
end
|
68
|
-
columns
|
69
|
-
end
|
70
|
-
|
71
|
-
def extract_headers(source_rows, columns, view)
|
72
|
-
headers = source_rows.first.css('th')
|
73
|
-
unless columns.nil?
|
74
|
-
headers = headers.to_a.values_at(*columns)
|
75
|
-
end
|
76
|
-
headers = headers.map {|th| th.content.strip}
|
77
|
-
if view == 'weekview'
|
78
|
-
headers = headers.map {|th| th.gsub(/\s+/, ' ') }
|
79
|
-
end
|
80
|
-
headers
|
81
|
-
end
|
82
|
-
|
83
|
-
def extract_data_rows(source_rows, columns)
|
84
|
-
data_rows = []
|
85
|
-
source_rows.each do |tr|
|
86
|
-
cells = tr.css('td')
|
87
|
-
next if cells.empty?
|
88
|
-
unless columns.nil?
|
89
|
-
cells = cells.to_a.values_at(*columns)
|
90
|
-
end
|
91
|
-
data_rows << fix_empty_cells(cells.map {|td| td.content.strip})
|
92
|
-
end
|
93
|
-
data_rows
|
94
|
-
end
|
95
|
-
|
96
|
-
def extract_summaries(source_rows, data_rows, columns)
|
97
|
-
summaries = nil
|
98
|
-
unless data_rows.empty?
|
99
|
-
summaries = source_rows.last.css('th')
|
100
|
-
unless columns.nil?
|
101
|
-
summaries = summaries.to_a.values_at(*columns)
|
102
|
-
end
|
103
|
-
summaries = summaries.map {|th| th.content.strip }
|
104
|
-
fix_empty_cells(summaries)
|
105
|
-
end
|
106
|
-
summaries
|
107
|
-
end
|
108
|
-
|
109
|
-
def get_page_for(date)
|
110
|
-
puts "Fetching data for #{date} ..."
|
111
|
-
self.date = date
|
112
|
-
@page = @form.submit
|
113
|
-
end
|
114
|
-
|
115
|
-
def day_field
|
116
|
-
@form.field_with(:name => 'viewdate[day]')
|
117
|
-
end
|
118
|
-
|
119
|
-
def month_field
|
120
|
-
@form.field_with(:name => 'viewdate[month]')
|
121
|
-
end
|
122
|
-
|
123
|
-
def year_field
|
124
|
-
@form.field_with(:name => 'viewdate[year]')
|
125
|
-
end
|
126
|
-
|
127
|
-
def fix_empty_cells(row)
|
128
|
-
row.collect! {|c| c == "\302\240" ? ' ' : c} # UTF-8 NO-BREAK-SPACE
|
129
|
-
end
|
130
|
-
|
131
|
-
end
|
@@ -1,227 +0,0 @@
|
|
1
|
-
require 'achoo/form'
|
2
|
-
require 'achoo/hour_administration_form'
|
3
|
-
|
4
|
-
class Achoo::HourRegistrationForm < Achoo::Form
|
5
|
-
|
6
|
-
def initialize(agent)
|
7
|
-
@agent = agent
|
8
|
-
@page = @agent.get(RC[:hour_registration_url])
|
9
|
-
@form = @page.form('entryform')
|
10
|
-
|
11
|
-
if @form.nil?
|
12
|
-
# Happens if the user has viewed a day or week report for a
|
13
|
-
# locked month. Fetching todays day report should fix this in
|
14
|
-
# most cases.
|
15
|
-
|
16
|
-
# FIX Ugly call to a private method using send()
|
17
|
-
haf = Achoo::HourAdministrationForm.new(@agent)
|
18
|
-
@page = haf.send(:set_page_to_view_for_date, 'dayview', Date.today)
|
19
|
-
@form = @page.form('entryform')
|
20
|
-
end
|
21
|
-
|
22
|
-
if @form.nil?
|
23
|
-
raise "Failed to retrieve the hour registration form.\nThe likely cause is that you have locked the current month, which is a silly thing to do."
|
24
|
-
end
|
25
|
-
|
26
|
-
@projects_seen = {}
|
27
|
-
@phases_seen = {}
|
28
|
-
|
29
|
-
# Need to preselect this for some reason
|
30
|
-
@form.field_with(:name => 'billpercent').options.first.select
|
31
|
-
# Preselecting this one as well, just in case
|
32
|
-
@form.field_with(:name => 'workperiod').options.first.select
|
33
|
-
end
|
34
|
-
|
35
|
-
def project
|
36
|
-
extract_number_from_projectid(@form.projectid)
|
37
|
-
end
|
38
|
-
|
39
|
-
def project=(projectid)
|
40
|
-
@form.projectid = "project.id='#{projectid}'"
|
41
|
-
end
|
42
|
-
|
43
|
-
def remark=(remark)
|
44
|
-
@form.remark = remark
|
45
|
-
end
|
46
|
-
|
47
|
-
def hours=(hours)
|
48
|
-
@form.time = hours
|
49
|
-
end
|
50
|
-
|
51
|
-
def phase
|
52
|
-
@form.phaseid.match(/phase\.id='(\d+)'/)[1]
|
53
|
-
end
|
54
|
-
|
55
|
-
def phase=(phaseid)
|
56
|
-
@form.phaseid = "phase.id='#{phaseid}'"
|
57
|
-
end
|
58
|
-
|
59
|
-
def workperiod=(workperiod)
|
60
|
-
@form.workperiod = "workperiod.id='#{workperiod}'"
|
61
|
-
end
|
62
|
-
|
63
|
-
def billing=(billing)
|
64
|
-
@form.billpercent = "billpercent.id='#{billing}'"
|
65
|
-
end
|
66
|
-
|
67
|
-
|
68
|
-
def worktime_periods
|
69
|
-
@form.field_with(:name => 'workperiod').options.collect do |opt|
|
70
|
-
[opt.value.match(/workperiod\.id='(\d+)'/)[1], opt.text]
|
71
|
-
end
|
72
|
-
end
|
73
|
-
|
74
|
-
def billing_options
|
75
|
-
@form.field_with(:name => 'billpercent').options.collect do |opt|
|
76
|
-
[opt.value.match(/billpercent\.id='(\d+)'/)[1], opt.text]
|
77
|
-
end
|
78
|
-
end
|
79
|
-
|
80
|
-
def phases_for_selected_project
|
81
|
-
partial_page = retrieve_project_phases_page
|
82
|
-
page = create_page_from_partial(partial_page)
|
83
|
-
field = page.forms.first.field_with(:name => 'phaseid')
|
84
|
-
|
85
|
-
phases = []
|
86
|
-
if field.respond_to?(:options)
|
87
|
-
field.options.each do |opt|
|
88
|
-
phases << [extract_number_from_phaseid(opt.value), opt.text]
|
89
|
-
end
|
90
|
-
else
|
91
|
-
partial_page.body.match(/(^[^<]+) </)
|
92
|
-
phases << [extract_number_from_phaseid(field.value), $1]
|
93
|
-
end
|
94
|
-
|
95
|
-
phases.each {|p| @phases_seen[p[0]] = p[1]}
|
96
|
-
|
97
|
-
return phases
|
98
|
-
end
|
99
|
-
|
100
|
-
def recent_projects
|
101
|
-
projects = []
|
102
|
-
@form.field_with(:name => 'projectid').options.each do |opt|
|
103
|
-
val = opt.value["project.id='".length..-2]
|
104
|
-
projects << [val, opt.text]
|
105
|
-
end
|
106
|
-
|
107
|
-
projects.each {|p| @projects_seen[p[0]] = p[1]}
|
108
|
-
|
109
|
-
projects
|
110
|
-
end
|
111
|
-
|
112
|
-
def all_projects
|
113
|
-
puts "Getting project page #1..."
|
114
|
-
projects_page = @agent.get(projects_url)
|
115
|
-
projects = scrape_projects(projects_page)
|
116
|
-
|
117
|
-
i = 2
|
118
|
-
while (link = projects_page.link_with(:text => 'Next'))
|
119
|
-
puts "Getting project page ##{i}..."
|
120
|
-
projects_page = link.click
|
121
|
-
projects.merge!(scrape_projects(projects_page))
|
122
|
-
i += 1
|
123
|
-
end
|
124
|
-
|
125
|
-
projects.keys.sort.collect do |name|
|
126
|
-
id = projects[name][0]
|
127
|
-
text = "#{projects[name][1]}: #{name}"
|
128
|
-
@projects_seen[id] = text
|
129
|
-
[id, text]
|
130
|
-
end
|
131
|
-
end
|
132
|
-
|
133
|
-
def print_values
|
134
|
-
format = "%10s: \"%s\"\n"
|
135
|
-
printf format, 'date', date_to_s
|
136
|
-
printf format, 'project', @projects_seen[project]
|
137
|
-
printf format, 'phase', @phases_seen[phase]
|
138
|
-
printf format, 'remark', @form.remark
|
139
|
-
printf format, 'hours', @form.time
|
140
|
-
printf format, 'worktime', @form.field_with(:name => 'workperiod').options.first.text
|
141
|
-
printf format, 'billing', @form.field_with(:name => 'billpercent').options.first.text
|
142
|
-
|
143
|
-
# @form.fields.each do |field|
|
144
|
-
# printf format, field.name, field.value
|
145
|
-
# end
|
146
|
-
|
147
|
-
end
|
148
|
-
|
149
|
-
def submit
|
150
|
-
@form.submit()
|
151
|
-
end
|
152
|
-
|
153
|
-
private
|
154
|
-
|
155
|
-
def date_to_s
|
156
|
-
date.strftime("%Y-%m-%d")
|
157
|
-
end
|
158
|
-
|
159
|
-
def retrieve_project_phases_page
|
160
|
-
old = {
|
161
|
-
:atkaction => @form.atkaction,
|
162
|
-
:action => @form.action,
|
163
|
-
}
|
164
|
-
|
165
|
-
@form.action = RC[:url]+"/dispatch.php?atkpartial=attribute.phaseid.refresh"
|
166
|
-
@form.atkaction = 'add'
|
167
|
-
partial_page = @form.submit
|
168
|
-
@form.action = old[:action]
|
169
|
-
@form.atkaction = old[:atkaction]
|
170
|
-
|
171
|
-
return partial_page
|
172
|
-
end
|
173
|
-
|
174
|
-
def create_page_from_partial(partial_page)
|
175
|
-
body = "<html><head></head><body><form>#{partial_page.body}</form></body></html>"
|
176
|
-
page = Mechanize::Page.new(nil, {'content-type' => 'text/html; charset=iso-8859-1'},
|
177
|
-
body, nil, @agent)
|
178
|
-
end
|
179
|
-
|
180
|
-
def day_field
|
181
|
-
@form.field_with(:name => 'activitydate[day]')
|
182
|
-
end
|
183
|
-
|
184
|
-
def month_field
|
185
|
-
@form.field_with(:name => 'activitydate[month]')
|
186
|
-
end
|
187
|
-
|
188
|
-
def year_field
|
189
|
-
@form.field_with(:name => 'activitydate[year]')
|
190
|
-
end
|
191
|
-
|
192
|
-
def extract_number_from_projectid(projectid)
|
193
|
-
projectid.match(/project\.id='(\d+)'/)[1]
|
194
|
-
end
|
195
|
-
|
196
|
-
def extract_number_from_phaseid(projectid)
|
197
|
-
projectid.match(/phase\.id='(\d+)'/)[1]
|
198
|
-
end
|
199
|
-
|
200
|
-
def projects_url
|
201
|
-
atk_submit_to_url(@page.link_with(:text => 'Select project').href)
|
202
|
-
end
|
203
|
-
|
204
|
-
def atk_submit_to_url(atk_submit)
|
205
|
-
href = atk_submit['javascript:atkSubmit("__'.length..-3]
|
206
|
-
href.gsub!('_13F', '?')
|
207
|
-
href.gsub!('_13D', '=')
|
208
|
-
href.gsub!('_126', '&')
|
209
|
-
href.gsub!('_125', '%')
|
210
|
-
return RC[:url] + '/' + href
|
211
|
-
end
|
212
|
-
|
213
|
-
def scrape_projects(projects_page)
|
214
|
-
projects = {}
|
215
|
-
projects_page.search('table#rl_1 tr').each do |tr|
|
216
|
-
cells = tr.search('td')
|
217
|
-
next if cells.empty?
|
218
|
-
projects[cells[1].text.strip] = [
|
219
|
-
cells[1].at_css('a').attribute('href').to_s.match('project.id%3D%27(\d+)%27')[1],
|
220
|
-
cells[0].text.strip,
|
221
|
-
]
|
222
|
-
end
|
223
|
-
|
224
|
-
return projects
|
225
|
-
end
|
226
|
-
|
227
|
-
end
|