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
@@ -0,0 +1,119 @@
|
|
1
|
+
require 'time'
|
2
|
+
|
3
|
+
class Achoo; end
|
4
|
+
|
5
|
+
class Achoo::Timespan
|
6
|
+
|
7
|
+
SECONDS_IN_A_DAY = 86400
|
8
|
+
SECONDS_IN_AN_HOUR = 3600
|
9
|
+
SECONDS_IN_A_MINUTE = 60
|
10
|
+
|
11
|
+
attr :start
|
12
|
+
attr :end
|
13
|
+
|
14
|
+
def initialize(start, end_)
|
15
|
+
raise ArgumentError.new('Nil in parameters not allowed') if start.nil? || end_.nil?
|
16
|
+
|
17
|
+
self.start = start
|
18
|
+
self.end = end_
|
19
|
+
end
|
20
|
+
|
21
|
+
def start=(timeish)
|
22
|
+
@start = to_time(timeish)
|
23
|
+
end
|
24
|
+
|
25
|
+
def end=(timeish)
|
26
|
+
@end = to_time(timeish)
|
27
|
+
end
|
28
|
+
|
29
|
+
def to_s
|
30
|
+
duration = duration_string
|
31
|
+
from_to = from_to_string
|
32
|
+
|
33
|
+
sprintf("(%s) %s", duration, from_to)
|
34
|
+
end
|
35
|
+
|
36
|
+
def contains?(timeish_or_timespan)
|
37
|
+
if timeish_or_timespan.is_a? Achoo::Timespan
|
38
|
+
timespan = timeish_or_timespan
|
39
|
+
return start <= timespan.start && self.end >= timespan.end
|
40
|
+
else
|
41
|
+
time = to_time(timeish_or_timespan)
|
42
|
+
return start <= time && self.end >= time
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def overlaps?(timespan)
|
47
|
+
start <= timespan.start && self.end >= timespan.start \
|
48
|
+
|| start <= timespan.end && self.end >= timespan.end \
|
49
|
+
|| contains?(timespan) || timespan.contains?(self)
|
50
|
+
end
|
51
|
+
|
52
|
+
private
|
53
|
+
|
54
|
+
def to_time(timeish)
|
55
|
+
case timeish
|
56
|
+
when Time
|
57
|
+
timeish.clone
|
58
|
+
when DateTime
|
59
|
+
Time.local(timeish.year, timeish.month, timeish.day, timeish.hour,timeish.minute, timeish.second)
|
60
|
+
when Date
|
61
|
+
Time.local(timeish.year, timeish.month, timeish.day)
|
62
|
+
else
|
63
|
+
if timeish.respond_to?(:to_s)
|
64
|
+
Time.parse(timeish.to_s)
|
65
|
+
else
|
66
|
+
raise ArgumentError.new("Don't know how to convert #{timeish.class} to Time")
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
|
72
|
+
def duration_string
|
73
|
+
delta = @end - @start
|
74
|
+
d = delta.to_i / SECONDS_IN_A_DAY
|
75
|
+
|
76
|
+
delta = delta - d*SECONDS_IN_A_DAY
|
77
|
+
h = delta.to_i / SECONDS_IN_AN_HOUR
|
78
|
+
|
79
|
+
delta = delta - h*SECONDS_IN_AN_HOUR
|
80
|
+
m = delta.to_i / SECONDS_IN_A_MINUTE
|
81
|
+
|
82
|
+
sprintf "%d+%02d:%02d", d, h, m
|
83
|
+
end
|
84
|
+
|
85
|
+
def from_to_string
|
86
|
+
today = Date.today
|
87
|
+
start_date = start.send(:to_date)
|
88
|
+
end_date = self.end.send(:to_date)
|
89
|
+
|
90
|
+
format = if start_date == today
|
91
|
+
"Today"
|
92
|
+
elsif start_date.month == today.month &&
|
93
|
+
start_date.year == today.year
|
94
|
+
"%a %e."
|
95
|
+
elsif start_date.year == today.year
|
96
|
+
"%a %e. %b"
|
97
|
+
else
|
98
|
+
"%a %e. %b %Y"
|
99
|
+
end
|
100
|
+
from = start.strftime(format << " %R")
|
101
|
+
|
102
|
+
format = if end_date == start_date
|
103
|
+
"%R"
|
104
|
+
elsif end_date == today
|
105
|
+
"Today %R"
|
106
|
+
elsif end_date.month == today.month &&
|
107
|
+
end_date.year == today.year
|
108
|
+
"%a %e. %R"
|
109
|
+
elsif end_date.year == today.year
|
110
|
+
"%a %e. %b %R"
|
111
|
+
else
|
112
|
+
"%a %e. %b %Y %R"
|
113
|
+
end
|
114
|
+
to = self.end.strftime(format)
|
115
|
+
|
116
|
+
sprintf "%s - %s", from, to
|
117
|
+
end
|
118
|
+
|
119
|
+
end
|
data/lib/achoo/ui.rb
ADDED
@@ -0,0 +1,10 @@
|
|
1
|
+
class Achoo; class UI; end; end
|
2
|
+
|
3
|
+
Achoo::UI.autoload :Commands, 'achoo/ui/commands'
|
4
|
+
Achoo::UI.autoload :Common, 'achoo/ui/common'
|
5
|
+
Achoo::UI.autoload :DateChooser, 'achoo/ui/date_chooser'
|
6
|
+
Achoo::UI.autoload :DateChoosers, 'achoo/ui/date_choosers'
|
7
|
+
Achoo::UI.autoload :ExceptionHandling, 'achoo/ui/exception_handling'
|
8
|
+
Achoo::UI.autoload :MonthChooser, 'achoo/ui/month_chooser'
|
9
|
+
Achoo::UI.autoload :OptionallyRangedDateChooser, 'achoo/ui/optionally_ranged_date_chooser'
|
10
|
+
Achoo::UI.autoload :RegisterHours, 'achoo/ui/register_hours'
|
@@ -0,0 +1,51 @@
|
|
1
|
+
require 'achoo/hour_administration_form'
|
2
|
+
require 'achoo/lock_month_form'
|
3
|
+
require 'achoo/ui'
|
4
|
+
|
5
|
+
module Achoo::UI::Commands
|
6
|
+
|
7
|
+
include Achoo::UI::DateChoosers
|
8
|
+
include Achoo::UI::Common
|
9
|
+
|
10
|
+
def show_registered_hours_for_day(agent)
|
11
|
+
date = date_chooser
|
12
|
+
form = Achoo::HourAdministrationForm.new(agent)
|
13
|
+
form.show_registered_hours_for_day(date)
|
14
|
+
end
|
15
|
+
|
16
|
+
def show_registered_hours_for_week(agent)
|
17
|
+
date = date_chooser
|
18
|
+
form = Achoo::HourAdministrationForm.new(agent)
|
19
|
+
form.show_registered_hours_for_week(date)
|
20
|
+
end
|
21
|
+
|
22
|
+
|
23
|
+
def show_flexi_time(agent)
|
24
|
+
date = date_chooser
|
25
|
+
form = Achoo::HourAdministrationForm.new(agent)
|
26
|
+
balance = form.flexi_time(date)
|
27
|
+
puts "Flexi time balance: #{Achoo::Term::underline(balance)}"
|
28
|
+
end
|
29
|
+
|
30
|
+
|
31
|
+
def lock_month(agent)
|
32
|
+
month = month_chooser
|
33
|
+
form = Achoo::LockMonthForm.new(agent)
|
34
|
+
form.lock_month(month)
|
35
|
+
form.print_values
|
36
|
+
if confirm
|
37
|
+
form.submit
|
38
|
+
else
|
39
|
+
puts "Cancelled"
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
|
44
|
+
def show_holiday_report(agent)
|
45
|
+
page = agent.get(RC[:holiday_report_url])
|
46
|
+
page.body.match(/<b>(\d+,\d+)<\/b>/)
|
47
|
+
puts "Balance: #{Achoo::Term::underline($1)}"
|
48
|
+
end
|
49
|
+
|
50
|
+
|
51
|
+
end
|
@@ -0,0 +1,75 @@
|
|
1
|
+
require 'achoo/term'
|
2
|
+
|
3
|
+
class Achoo; class UI; end; end
|
4
|
+
|
5
|
+
class Achoo::UI::DateChooser
|
6
|
+
|
7
|
+
PROMPT = "Date ([today] | ?)"
|
8
|
+
FORMAT = " today | (+|-)n | [[[YY]YY]-[M]M]-[D]D"
|
9
|
+
|
10
|
+
def choose
|
11
|
+
loop do
|
12
|
+
answer = Achoo::Term::ask PROMPT
|
13
|
+
begin
|
14
|
+
date = handle_answer(answer)
|
15
|
+
return date if date
|
16
|
+
rescue ArgumentError => e
|
17
|
+
puts e
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def parse_date(date_str, base=Date.today)
|
23
|
+
raise ArgumentError.new('Invalid date') if date_str.nil?
|
24
|
+
|
25
|
+
# Today (default)
|
26
|
+
if date_str == 'today' || date_str.empty?
|
27
|
+
return Date.today
|
28
|
+
end
|
29
|
+
|
30
|
+
# Base offset
|
31
|
+
case date_str.chars.first
|
32
|
+
when '-'
|
33
|
+
return base - Integer(date_str[1..-1])
|
34
|
+
when '+'
|
35
|
+
return base + Integer(date_str[1..-1])
|
36
|
+
end
|
37
|
+
|
38
|
+
#
|
39
|
+
date = date_str.split('-').collect {|d| d.to_i}
|
40
|
+
case date.length
|
41
|
+
when 1
|
42
|
+
return Date.civil(base.year, base.month, *date)
|
43
|
+
when 2
|
44
|
+
return Date.civil(base.year, *date)
|
45
|
+
when 3
|
46
|
+
date[0] += 2000 if date[0] < 100
|
47
|
+
return Date.civil(*date)
|
48
|
+
end
|
49
|
+
|
50
|
+
raise ArgumentError.new('Invalid date')
|
51
|
+
end
|
52
|
+
|
53
|
+
private
|
54
|
+
|
55
|
+
def handle_answer(answer)
|
56
|
+
if answer == '?'
|
57
|
+
print_help_message
|
58
|
+
return false
|
59
|
+
else
|
60
|
+
return parse_date(answer)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def print_help_message
|
65
|
+
puts "Accepted formats:"
|
66
|
+
puts date_format_help_string
|
67
|
+
puts
|
68
|
+
system 'cal -3m'
|
69
|
+
end
|
70
|
+
|
71
|
+
def date_format_help_string
|
72
|
+
FORMAT
|
73
|
+
end
|
74
|
+
|
75
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
require 'achoo/ui/date_chooser'
|
2
|
+
require 'achoo/ui/optionally_ranged_date_chooser'
|
3
|
+
require 'achoo/ui/month_chooser'
|
4
|
+
|
5
|
+
class Achoo; class UI; end; end
|
6
|
+
|
7
|
+
module Achoo::UI::DateChoosers
|
8
|
+
|
9
|
+
def date_chooser
|
10
|
+
Achoo::UI::DateChooser.new.choose
|
11
|
+
end
|
12
|
+
|
13
|
+
def optionally_ranged_date_chooser
|
14
|
+
Achoo::UI::OptionallyRangedDateChooser.new.choose
|
15
|
+
end
|
16
|
+
|
17
|
+
def month_chooser
|
18
|
+
Achoo::UI::MonthChooser.new.choose
|
19
|
+
end
|
20
|
+
|
21
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
require 'achoo/term'
|
2
|
+
|
3
|
+
class Achoo; class UI; end; end;
|
4
|
+
|
5
|
+
module Achoo::UI::ExceptionHandling
|
6
|
+
|
7
|
+
def handle_exception(user_message, e)
|
8
|
+
Achoo::Term::warn(user_message) + get_exception_reason(e)
|
9
|
+
end
|
10
|
+
|
11
|
+
def handle_fatal_exception(user_message, e)
|
12
|
+
puts Achoo::Term::fatal(user_message) + get_exception_reason(e)
|
13
|
+
exit 1
|
14
|
+
end
|
15
|
+
|
16
|
+
def get_exception_reason(e)
|
17
|
+
"\nReason: \n\t" + e.message.gsub("\n", "\n\t") + "\n---\n\t" + e.backtrace.join("\n\t")
|
18
|
+
end
|
19
|
+
|
20
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
require 'achoo/term'
|
2
|
+
|
3
|
+
class Achoo; class UI; end; end
|
4
|
+
|
5
|
+
class Achoo::UI::MonthChooser
|
6
|
+
|
7
|
+
def choose
|
8
|
+
default = one_month_ago
|
9
|
+
period = Achoo::Term::ask "Period ([#{default}] | YYYYMM)"
|
10
|
+
period = default if !period || period.empty?
|
11
|
+
# FIX validate YYYYMM
|
12
|
+
period
|
13
|
+
end
|
14
|
+
|
15
|
+
def one_month_ago
|
16
|
+
now = Time.now
|
17
|
+
year = now.year
|
18
|
+
|
19
|
+
# Use -2 + 1 to shift range from 0-11 to 1-12
|
20
|
+
month = (now.month - 2)%12 + 1
|
21
|
+
year -= 1 if month > now.month
|
22
|
+
|
23
|
+
sprintf "%d%02d", year, month
|
24
|
+
end
|
25
|
+
|
26
|
+
end
|
27
|
+
|
@@ -0,0 +1,33 @@
|
|
1
|
+
require 'achoo/ui/date_chooser'
|
2
|
+
|
3
|
+
class Achoo::UI::OptionallyRangedDateChooser < Achoo::UI::DateChooser
|
4
|
+
|
5
|
+
def parse_date_range(date_range_str)
|
6
|
+
start_date_str, finish_date_str = *date_range_str.split('->')
|
7
|
+
start_date = parse_date(start_date_str.strip)
|
8
|
+
finish_date = parse_date(finish_date_str.strip, start_date)
|
9
|
+
|
10
|
+
if start_date >= finish_date
|
11
|
+
raise ArgumentError.new('Invalid date range')
|
12
|
+
end
|
13
|
+
|
14
|
+
[start_date, finish_date]
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
def handle_answer(answer)
|
20
|
+
if answer.include? '->'
|
21
|
+
return parse_date_range(answer)
|
22
|
+
else
|
23
|
+
return super
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def date_format_help_string
|
28
|
+
return " DATE [-> DATE]\n" \
|
29
|
+
<< " DATE:\n" \
|
30
|
+
<< FORMAT
|
31
|
+
end
|
32
|
+
|
33
|
+
end
|
@@ -0,0 +1,137 @@
|
|
1
|
+
require 'achoo/awake'
|
2
|
+
require 'achoo/hour_registration_form'
|
3
|
+
require 'achoo/hour_registration_form_ranged'
|
4
|
+
require 'achoo/ical'
|
5
|
+
require 'achoo/term'
|
6
|
+
require 'achoo/ui'
|
7
|
+
require 'achoo/vcs'
|
8
|
+
require 'stringio'
|
9
|
+
|
10
|
+
module Achoo::UI::RegisterHours
|
11
|
+
|
12
|
+
include Achoo::UI::DateChoosers
|
13
|
+
include Achoo::UI::ExceptionHandling
|
14
|
+
|
15
|
+
def register_hours(agent)
|
16
|
+
date = optionally_ranged_date_chooser
|
17
|
+
|
18
|
+
puts "Fetching data ..."
|
19
|
+
form = if date.class == Date
|
20
|
+
Achoo::HourRegistrationForm
|
21
|
+
else
|
22
|
+
Achoo::HourRegistrationFormRanged
|
23
|
+
end.new(agent)
|
24
|
+
|
25
|
+
form.date = date
|
26
|
+
form.project = project_chooser(form)
|
27
|
+
form.phase = phase_chooser(form)
|
28
|
+
print_remark_help(date) if date.class == Date
|
29
|
+
form.remark = remark_chooser
|
30
|
+
print_hours_help(date) if date.class == Date
|
31
|
+
form.hours = hours_chooser
|
32
|
+
|
33
|
+
answer = Achoo::Term.ask("Do you want to change the defaults for worktime period and/or billing percentage? [N/y]").downcase
|
34
|
+
if answer == 'y'
|
35
|
+
form.workperiod = workperiod_chooser(form)
|
36
|
+
form.billing = billing_chooser(form)
|
37
|
+
end
|
38
|
+
|
39
|
+
form.print_values
|
40
|
+
if confirm
|
41
|
+
puts "Submitting ..."
|
42
|
+
form.submit
|
43
|
+
else
|
44
|
+
puts "Cancelled"
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
|
49
|
+
def phase_chooser(form)
|
50
|
+
phases = form.phases_for_selected_project
|
51
|
+
puts "Phases"
|
52
|
+
answer = Achoo::Term.choose('Phase', phases.collect {|p| p[1] })
|
53
|
+
phases[answer.to_i-1][0]
|
54
|
+
end
|
55
|
+
|
56
|
+
|
57
|
+
def workperiod_chooser(form)
|
58
|
+
periods = form.worktime_periods
|
59
|
+
puts "Worktime periods"
|
60
|
+
answer = Achoo::Term.choose('Period [1]', periods.collect {|p| p[1] }, nil, [''])
|
61
|
+
answer = '1' if answer.empty?
|
62
|
+
periods[answer.to_i-1][0]
|
63
|
+
end
|
64
|
+
|
65
|
+
def billing_chooser(form)
|
66
|
+
options = form.billing_options
|
67
|
+
puts "Billing options"
|
68
|
+
answer = Achoo::Term.choose('Billing [1]', options.collect {|p| p[1] }, nil, [''])
|
69
|
+
answer = '1' if answer.empty?
|
70
|
+
options[answer.to_i-1][0]
|
71
|
+
end
|
72
|
+
|
73
|
+
|
74
|
+
def print_hours_help(date)
|
75
|
+
puts "Awake log:"
|
76
|
+
begin
|
77
|
+
awake = Achoo::Awake.new
|
78
|
+
awake.at(date)
|
79
|
+
puts
|
80
|
+
rescue Exception => e
|
81
|
+
print handle_exception("Failed to retrieve awake log.", e)
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
def hours_chooser
|
86
|
+
answer = Achoo::Term::ask 'Hours [7:30]'
|
87
|
+
return answer == '' ? '7.5' : answer
|
88
|
+
end
|
89
|
+
|
90
|
+
|
91
|
+
def print_remark_help(date)
|
92
|
+
puts "VCS logs for #{date}:"
|
93
|
+
begin
|
94
|
+
Achoo::VCS.print_logs_for(date, RC[:vcs_dirs])
|
95
|
+
rescue Exception => e
|
96
|
+
puts handle_exception("Failed to retrieve VCS logs.", e)
|
97
|
+
end
|
98
|
+
puts '-' * 80
|
99
|
+
puts "Calendar events for #{date}:"
|
100
|
+
puts '---'
|
101
|
+
begin
|
102
|
+
RC[:ical].each do |config|
|
103
|
+
Achoo::ICal.from_http_request(config).print_events(date)
|
104
|
+
end
|
105
|
+
rescue Exception => e
|
106
|
+
puts handle_exception("Failed to retrieve calendar events.", e)
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
def remark_chooser
|
111
|
+
Achoo::Term::ask 'Remark'
|
112
|
+
end
|
113
|
+
|
114
|
+
|
115
|
+
def project_chooser(form)
|
116
|
+
puts 'Recently used projects'
|
117
|
+
projects = form.recent_projects
|
118
|
+
answer = Achoo::Term.choose('Project [1]', projects.collect { |p| p[1] },
|
119
|
+
'Other', [''])
|
120
|
+
case answer
|
121
|
+
when ''
|
122
|
+
projects[0][0]
|
123
|
+
when '0'
|
124
|
+
return all_projects_chooser(form)
|
125
|
+
else
|
126
|
+
return projects[answer.to_i-1][0]
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
|
131
|
+
def all_projects_chooser(form)
|
132
|
+
projects = form.all_projects
|
133
|
+
answer = Achoo::Term.choose('Project', projects.collect { |p| p[1] })
|
134
|
+
projects[answer.to_i-1][0]
|
135
|
+
end
|
136
|
+
|
137
|
+
end
|