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.
Files changed (53) hide show
  1. data/CHANGES +12 -0
  2. data/README.rdoc +26 -30
  3. data/Rakefile +3 -0
  4. data/bin/achoo +2 -2
  5. data/lib/achoo.rb +1 -139
  6. data/lib/achoo/achievo.rb +13 -0
  7. data/lib/achoo/achievo/form.rb +22 -0
  8. data/lib/achoo/achievo/hour_administration_form.rb +91 -0
  9. data/lib/achoo/achievo/hour_registration_form.rb +230 -0
  10. data/lib/achoo/achievo/hour_registration_form_ranged.rb +49 -0
  11. data/lib/achoo/achievo/lock_month_form.rb +44 -0
  12. data/lib/achoo/achievo/login_form.rb +27 -0
  13. data/lib/achoo/achievo/table.rb +30 -0
  14. data/lib/achoo/app.rb +153 -0
  15. data/lib/achoo/awake.rb +86 -100
  16. data/lib/achoo/extensions.rb +24 -0
  17. data/lib/achoo/ical.rb +47 -40
  18. data/lib/achoo/rc_loader.rb +42 -42
  19. data/lib/achoo/system.rb +8 -3
  20. data/lib/achoo/system/cstruct.rb +67 -0
  21. data/lib/achoo/system/log_entry.rb +24 -0
  22. data/lib/achoo/system/pm_suspend.rb +17 -20
  23. data/lib/achoo/system/utmp_record.rb +64 -0
  24. data/lib/achoo/system/wtmp.rb +14 -10
  25. data/lib/achoo/temporal.rb +8 -0
  26. data/lib/achoo/temporal/open_timespan.rb +16 -0
  27. data/lib/achoo/temporal/timespan.rb +122 -0
  28. data/lib/achoo/term.rb +58 -56
  29. data/lib/achoo/term/menu.rb +2 -2
  30. data/lib/achoo/term/table.rb +59 -60
  31. data/lib/achoo/ui.rb +13 -9
  32. data/lib/achoo/ui/commands.rb +60 -38
  33. data/lib/achoo/ui/common.rb +10 -7
  34. data/lib/achoo/ui/date_chooser.rb +69 -65
  35. data/lib/achoo/ui/date_choosers.rb +15 -14
  36. data/lib/achoo/ui/exception_handling.rb +14 -12
  37. data/lib/achoo/ui/month_chooser.rb +37 -24
  38. data/lib/achoo/ui/optionally_ranged_date_chooser.rb +29 -25
  39. data/lib/achoo/ui/register_hours.rb +116 -114
  40. data/lib/achoo/vcs.rb +32 -30
  41. data/lib/achoo/vcs/git.rb +18 -14
  42. data/lib/achoo/vcs/subversion.rb +25 -23
  43. metadata +30 -24
  44. data/lib/achoo/binary.rb +0 -7
  45. data/lib/achoo/binary/cstruct.rb +0 -60
  46. data/lib/achoo/binary/utmp_record.rb +0 -59
  47. data/lib/achoo/form.rb +0 -18
  48. data/lib/achoo/hour_administration_form.rb +0 -131
  49. data/lib/achoo/hour_registration_form.rb +0 -227
  50. data/lib/achoo/hour_registration_form_ranged.rb +0 -45
  51. data/lib/achoo/lock_month_form.rb +0 -40
  52. data/lib/achoo/open_timespan.rb +0 -13
  53. data/lib/achoo/timespan.rb +0 -119
data/CHANGES CHANGED
@@ -1,3 +1,15 @@
1
+ 2010-04-20: version 0.4.1
2
+
3
+ - Bug fixes
4
+
5
+ 2010-04-20: version 0.4
6
+
7
+ - Converted rc file format to YAML
8
+
9
+ - Prefetch and cache iCalendar data in a parallel thread at startup
10
+
11
+ - Added support for 'Time Survey' reports
12
+
1
13
  2010-04-05: version 0.3
2
14
 
3
15
  - More compact time span format in awake log
data/README.rdoc CHANGED
@@ -9,18 +9,15 @@ These instructions are for installing on Ubuntu 9.10 (Karmic Koala),
9
9
  but they will probably work with minor adjustments on other systems as
10
10
  well.
11
11
 
12
- - Make sure that you have ruby and rubygems installed
12
+ - Make sure that you have ruby and rubygems installed together with
13
+ achoo's non-gem requirements:
13
14
 
14
- sudo aptitude install ruby rubygems
15
+ sudo aptitude install ruby rubygems ruby1.8-dev libxml2 \
16
+ libxml2-dev libxslt1 libxslt1-dev libopenssl-ruby
15
17
 
16
- - Install non-gem requirements
18
+ - Install Achoo
17
19
 
18
- sudo aptitude install ruby1.8-dev libxml2 libxml2-dev libxslt1 libxslt1-dev libopenssl-ruby
19
-
20
- - Download achoo-0.2.gem[http://github.com/downloads/kjellm/achoo/achoo-0.2.gem] and install it
21
-
22
- wget http://github.com/downloads/kjellm/achoo/achoo-0.2.gem
23
- gem install achoo-0.2.gem
20
+ gem install achoo
24
21
 
25
22
  - Make sure that ~/.gem/ruby/1.8/bin is in your PATH environment variable
26
23
 
@@ -31,9 +28,9 @@ well.
31
28
 
32
29
  === TRACKING THE LATEST CHANGES
33
30
 
34
- Requires git and rake
31
+ Requires git
35
32
 
36
- sudo aptitude install rake git-core
33
+ sudo aptitude install git-core
37
34
 
38
35
  Then
39
36
 
@@ -45,30 +42,28 @@ Or if you already has cloned achoo, update with
45
42
 
46
43
  Generate gem and install it
47
44
 
48
- rake build:gem
49
- gem install pkg/achoo-XXX.gem
45
+ gem build achoo.gemspec
46
+ gem install achoo-XXX.gem
50
47
 
51
48
  == USAGE
52
49
 
53
50
  First, make sure that language in Achievo's user preferences is
54
51
  English and theme is 'no value'.
55
52
 
56
- Create ~/.achoo from the following template and edit it:
57
-
58
- RC = {
59
- :url => 'https://example.com/achievo/',
60
- :user => 'joe',
61
- :password => 'geheim',
62
- :vcs_dirs => ["#{ENV['HOME']}/projects"],
63
- :ical => [{ :host => "foo.example.com",
64
- :port => 443,
65
- :path => "/joe/Calendar",
66
- :user => "joe",
67
- :pass => 'GeHeIm',
68
- },
69
- ],
70
- }
71
-
53
+ Create ~/.achoo from the following template (YAML) and edit it:
54
+
55
+ ---
56
+ :url : 'https://example.com/achievo/'
57
+ :user : 'joe'
58
+ :password : 'geheim'
59
+ :vcs_dirs : ["/home/joe/projects"],
60
+ :ical :
61
+ - :host : "foo.example.com"
62
+ :port : 443
63
+ :path : "/joe/Calendar"
64
+ :user : "joe"
65
+ :pass : 'GeHeIm'
66
+
72
67
  Then
73
68
 
74
69
  chmod 0600 ~/.achoo
@@ -78,7 +73,8 @@ You can then run achoo from a shell with the achoo command
78
73
  achoo
79
74
 
80
75
  For more usage information, see
81
- this[http://oierud.name/~kjellm/bliki/AchooTheAchievoCLI.html] blog.
76
+ this[http://oierud.name/~kjellm/bliki/AchooTheAchievoCLI.html] blog
77
+ post.
82
78
 
83
79
  == SET UP DEVELOPMENT ENVIRONMENT
84
80
 
data/Rakefile CHANGED
@@ -62,3 +62,6 @@ desc 'Install development dependencies'
62
62
  task :setup do
63
63
  system 'gem install shoulda rack thin redgreen allison'
64
64
  end
65
+
66
+ desc 'Remove generated files and folders'
67
+ task :clean => ['build:clobber_package', 'doc:clobber_rdoc']
data/bin/achoo CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
- require 'achoo'
3
+ require 'achoo/app'
4
4
  require 'achoo/rc_loader'
5
5
  require 'optparse'
6
6
 
@@ -18,7 +18,7 @@ def main
18
18
 
19
19
  load_rc(options[:rc_file])
20
20
 
21
- achoo = Achoo.new(options[:log])
21
+ achoo = Achoo::App.new(options[:log])
22
22
  achoo.start
23
23
  end
24
24
 
data/lib/achoo.rb CHANGED
@@ -1,140 +1,2 @@
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
-
1
+ module Achoo
139
2
  end
140
-
@@ -0,0 +1,13 @@
1
+ require 'achoo'
2
+
3
+ module Achoo
4
+ module Achievo
5
+ autoload :Form, 'achoo/achievo/form.rb'
6
+ autoload :HourAdministrationForm, 'achoo/achievo/hour_administration_form.rb'
7
+ autoload :HourRegistrationForm, 'achoo/achievo/hour_registration_form.rb'
8
+ autoload :HourRegistrationFormRanged, 'achoo/achievo/hour_registration_form_ranged.rb'
9
+ autoload :LockMonthForm, 'achoo/achievo/lock_month_form.rb'
10
+ autoload :LoginForm, 'achoo/achievo/login_form.rb'
11
+ autoload :Table, 'achoo/achievo/table.rb'
12
+ end
13
+ end
@@ -0,0 +1,22 @@
1
+ require 'achoo/achievo'
2
+
3
+ module Achoo
4
+ module Achievo
5
+ class Form
6
+
7
+ def date=(date)
8
+ # Day and month must be prefixed with '0' if single
9
+ # digit. Date.day and Date.month doesn't do this. Use strftime
10
+ day_field.value = date.strftime('%d')
11
+ month_field.value = date.strftime('%m')
12
+ year_field.value = date.year
13
+ end
14
+
15
+ def date
16
+ Date.new(year_field.value.to_i, month_field.value.to_i,
17
+ day_field.value.to_i)
18
+ end
19
+
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,91 @@
1
+ require 'achoo/achievo'
2
+ require 'achoo/term/table'
3
+
4
+ module Achoo
5
+ module Achievo
6
+ class HourAdministrationForm < Form
7
+
8
+ def initialize(agent)
9
+ @agent = agent
10
+ @page = nil
11
+ end
12
+
13
+ def show_registered_hours_for_day(date)
14
+ show_registered_hours(date, 'dayview', '#rl_1 tr')
15
+ end
16
+
17
+ def show_registered_hours_for_week(date)
18
+ show_registered_hours(date, 'weekview', '//form[@name="weekview"]/following::table/tr')
19
+ end
20
+
21
+ def flexi_time(date)
22
+ set_page_to_view_for_date('dayview', date)
23
+
24
+ @page.body.match(/Flexi time balance: (-?\d+:\d+)/)[1]
25
+ end
26
+
27
+ private
28
+
29
+ def show_registered_hours(date, view, query)
30
+ set_page_to_view_for_date(view, date)
31
+
32
+ table = Table.new(@page.search(query))
33
+
34
+ if view == 'weekview'
35
+ table.first.each {|c| c.gsub!(/\s+/, ' ') }
36
+ end
37
+
38
+ if view == 'dayview'
39
+ table.select_columns do |c|
40
+ # '' -> Ruby 1.9, ' ' -> Ruby 1.8
41
+ !['', ' ', 'Billing billed', 'Billing marked', 'Billing total'].include?(c[0])
42
+
43
+ end
44
+ end
45
+
46
+ summaries = table.length > 1 ? table.last : nil
47
+
48
+ Term::Table.new(table.first,
49
+ table[1 .. table.length-2],
50
+ summaries).print
51
+ end
52
+
53
+ def set_page_to_view_for_date(view, date)
54
+ @page ||= @agent.get(RC[:hour_admin_url])
55
+
56
+ link = @page.link_with(:text => view.capitalize)
57
+ @form = @page.form(view)
58
+ unless link.nil?
59
+ puts "Fetching #{view} ..."
60
+ @page = link.click
61
+ @form = @page.form(view)
62
+ end
63
+ unless date == self.date
64
+ @page = get_page_for(date)
65
+ @form = @page.form(view)
66
+ end
67
+
68
+ @page
69
+ end
70
+
71
+ def get_page_for(date)
72
+ puts "Fetching data for #{date} ..."
73
+ self.date = date
74
+ @page = @form.submit
75
+ end
76
+
77
+ def day_field
78
+ @form.field_with(:name => 'viewdate[day]')
79
+ end
80
+
81
+ def month_field
82
+ @form.field_with(:name => 'viewdate[month]')
83
+ end
84
+
85
+ def year_field
86
+ @form.field_with(:name => 'viewdate[year]')
87
+ end
88
+
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,230 @@
1
+ require 'achoo/achievo'
2
+
3
+ module Achoo
4
+ module Achievo
5
+ class HourRegistrationForm < Form
6
+
7
+ def initialize(agent)
8
+ @agent = agent
9
+ @page = @agent.get(RC[:hour_registration_url])
10
+ @form = @page.form('entryform')
11
+
12
+ if @form.nil?
13
+ # Happens if the user has viewed a day or week report for a
14
+ # locked month. Fetching todays day report should fix this in
15
+ # most cases.
16
+
17
+ # FIX Ugly call to a private method using send()
18
+ haf = HourAdministrationForm.new(@agent)
19
+ @page = haf.send(:set_page_to_view_for_date, 'dayview', Date.today)
20
+ @form = @page.form('entryform')
21
+ end
22
+
23
+ if @form.nil?
24
+ 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."
25
+ end
26
+
27
+ @projects_seen = {}
28
+ @phases_seen = {}
29
+
30
+ # Need to preselect this for some reason
31
+ @form.field_with(:name => 'billpercent').options.first.select
32
+ # Preselecting this one as well, just in case
33
+ @form.field_with(:name => 'workperiod').options.first.select
34
+ end
35
+
36
+ def project
37
+ extract_number_from_projectid(@form.projectid)
38
+ end
39
+
40
+ def project=(projectid)
41
+ @form.projectid = "project.id='#{projectid}'"
42
+ end
43
+
44
+ def remark=(remark)
45
+ @form.remark = remark
46
+ end
47
+
48
+ def hours=(hours)
49
+ @form.time = hours
50
+ end
51
+
52
+ def phase
53
+ @form.phaseid.match(/phase\.id='(\d+)'/)[1]
54
+ end
55
+
56
+ def phase=(phaseid)
57
+ @form.phaseid = "phase.id='#{phaseid}'"
58
+ end
59
+
60
+ def workperiod=(workperiod)
61
+ @form.workperiod = "workperiod.id='#{workperiod}'"
62
+ end
63
+
64
+ def billing=(billing)
65
+ @form.billpercent = "billpercent.id='#{billing}'"
66
+ end
67
+
68
+
69
+ def worktime_periods
70
+ @form.field_with(:name => 'workperiod').options.collect do |opt|
71
+ [opt.value.match(/workperiod\.id='(\d+)'/)[1], opt.text]
72
+ end
73
+ end
74
+
75
+ def billing_options
76
+ @form.field_with(:name => 'billpercent').options.collect do |opt|
77
+ [opt.value.match(/billpercent\.id='(\d+)'/)[1], opt.text]
78
+ end
79
+ end
80
+
81
+ def phases_for_selected_project
82
+ partial_page = retrieve_project_phases_page
83
+ page = create_page_from_partial(partial_page)
84
+ field = page.forms.first.field_with(:name => 'phaseid')
85
+
86
+ phases = []
87
+ if field.respond_to?(:options)
88
+ field.options.each do |opt|
89
+ phases << [extract_number_from_phaseid(opt.value), opt.text]
90
+ end
91
+ else
92
+ partial_page.body.match(/(^[^<]+)&nbsp;&nbsp;</)
93
+ phases << [extract_number_from_phaseid(field.value), $1]
94
+ end
95
+
96
+ phases.each {|p| @phases_seen[p[0]] = p[1]}
97
+
98
+ return phases
99
+ end
100
+
101
+ def recent_projects
102
+ projects = []
103
+ @form.field_with(:name => 'projectid').options.each do |opt|
104
+ val = opt.value["project.id='".length..-2]
105
+ projects << [val, opt.text]
106
+ end
107
+
108
+ projects.each {|p| @projects_seen[p[0]] = p[1]}
109
+
110
+ projects
111
+ end
112
+
113
+ def all_projects
114
+ puts "Getting project page #1..."
115
+ projects_page = @agent.get(projects_url)
116
+ projects = scrape_projects(projects_page)
117
+
118
+ i = 2
119
+ while (link = projects_page.link_with(:text => 'Next'))
120
+ puts "Getting project page ##{i}..."
121
+ projects_page = link.click
122
+ projects.merge!(scrape_projects(projects_page))
123
+ i += 1
124
+ end
125
+
126
+ projects.keys.sort.collect do |name|
127
+ id = projects[name][0]
128
+ text = "#{projects[name][1]}: #{name}"
129
+ @projects_seen[id] = text
130
+ [id, text]
131
+ end
132
+ end
133
+
134
+ def print_values
135
+ format = "%10s: \"%s\"\n"
136
+ printf format, 'date', date_to_s
137
+ printf format, 'project', @projects_seen[project]
138
+ printf format, 'phase', @phases_seen[phase]
139
+ printf format, 'remark', @form.remark
140
+ printf format, 'hours', @form.time
141
+ printf format, 'worktime', @form.field_with(:name => 'workperiod').options.first.text
142
+ printf format, 'billing', @form.field_with(:name => 'billpercent').options.first.text
143
+
144
+ # @form.fields.each do |field|
145
+ # printf format, field.name, field.value
146
+ # end
147
+
148
+ end
149
+
150
+ def submit
151
+ @form.submit()
152
+ end
153
+
154
+ private
155
+
156
+ def date_to_s
157
+ date.strftime("%Y-%m-%d")
158
+ end
159
+
160
+ def retrieve_project_phases_page
161
+ old = {
162
+ :atkaction => @form.atkaction,
163
+ :action => @form.action,
164
+ }
165
+
166
+ @form.action = RC[:url]+"/dispatch.php?atkpartial=attribute.phaseid.refresh"
167
+ @form.atkaction = 'add'
168
+ partial_page = @form.submit
169
+ @form.action = old[:action]
170
+ @form.atkaction = old[:atkaction]
171
+
172
+ return partial_page
173
+ end
174
+
175
+ def create_page_from_partial(partial_page)
176
+ body = "<html><head></head><body><form>#{partial_page.body}</form></body></html>"
177
+ page = Mechanize::Page.new(nil, {'content-type' => 'text/html; charset=iso-8859-1'},
178
+ body, nil, @agent)
179
+ end
180
+
181
+ def day_field
182
+ @form.field_with(:name => 'activitydate[day]')
183
+ end
184
+
185
+ def month_field
186
+ @form.field_with(:name => 'activitydate[month]')
187
+ end
188
+
189
+ def year_field
190
+ @form.field_with(:name => 'activitydate[year]')
191
+ end
192
+
193
+ def extract_number_from_projectid(projectid)
194
+ projectid.match(/project\.id='(\d+)'/)[1]
195
+ end
196
+
197
+ def extract_number_from_phaseid(projectid)
198
+ projectid.match(/phase\.id='(\d+)'/)[1]
199
+ end
200
+
201
+ def projects_url
202
+ atk_submit_to_url(@page.link_with(:text => 'Select project').href)
203
+ end
204
+
205
+ def atk_submit_to_url(atk_submit)
206
+ href = atk_submit['javascript:atkSubmit("__'.length..-3]
207
+ href.gsub!('_13F', '?')
208
+ href.gsub!('_13D', '=')
209
+ href.gsub!('_126', '&')
210
+ href.gsub!('_125', '%')
211
+ return RC[:url] + '/' + href
212
+ end
213
+
214
+ def scrape_projects(projects_page)
215
+ projects = {}
216
+ projects_page.search('table#rl_1 tr').each do |tr|
217
+ cells = tr.search('td')
218
+ next if cells.empty?
219
+ projects[cells[1].text.strip] = [
220
+ cells[1].at_css('a').attribute('href').to_s.match('project.id%3D%27(\d+)%27')[1],
221
+ cells[0].text.strip,
222
+ ]
223
+ end
224
+
225
+ return projects
226
+ end
227
+
228
+ end
229
+ end
230
+ end