achoo 0.3 → 0.4.1

Sign up to get free protection for your applications and to get access to all the features.
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