hcl 0.2.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/.document +1 -0
- data/.gitignore +5 -0
- data/CHANGELOG +41 -0
- data/LICENSE +19 -0
- data/README.markdown +101 -0
- data/Rakefile +24 -0
- data/VERSION +1 -0
- data/bin/hcl +6 -0
- data/deps.rip +3 -0
- data/hcl.gemspec +75 -0
- data/lib/hcl/app.rb +164 -0
- data/lib/hcl/commands.rb +90 -0
- data/lib/hcl/day_entry.rb +66 -0
- data/lib/hcl/project.rb +3 -0
- data/lib/hcl/task.rb +58 -0
- data/lib/hcl/timesheet_resource.rb +74 -0
- data/lib/hcl/utility.rb +25 -0
- data/test/app_test.rb +8 -0
- data/test/day_entry_test.rb +49 -0
- data/test/test_helper.rb +6 -0
- data/test/utility_test.rb +17 -0
- metadata +118 -0
data/.document
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--files CHANGELOG
|
data/CHANGELOG
ADDED
@@ -0,0 +1,41 @@
|
|
1
|
+
= Recent Changes in HCl
|
2
|
+
|
3
|
+
== v0.2.3 Sun Aug 23 21:39:34 2009 -0700
|
4
|
+
|
5
|
+
* Allow decimal time offset without a dot, closes #29.
|
6
|
+
* Reverted and re-fixed: Adding note fails when task is started without notes, #26.
|
7
|
+
* Reinstate the --version option
|
8
|
+
|
9
|
+
== v0.2.2 Sun Aug 9 11:16:34 2009 -0700
|
10
|
+
|
11
|
+
* Support installation via rip, closes #27.
|
12
|
+
* Fixed: Adding note fails when task is started without notes, closes #26.
|
13
|
+
* Avoid stack trace on missing XML root node, closes #25.
|
14
|
+
|
15
|
+
== v0.2.1 Thu Jul 30 14:02:23 2009 -0700
|
16
|
+
|
17
|
+
* Fixed: Creating timers without starting them.
|
18
|
+
|
19
|
+
== v0.2.0 Thu Jul 30 11:40:33 2009 -0700
|
20
|
+
|
21
|
+
* Allow an initial time to be specified when starting a timer, closes #9.
|
22
|
+
* Always display hours as HH:MM, closes #22.
|
23
|
+
* Do not write empty task cache, closes #23.
|
24
|
+
|
25
|
+
== v0.1.3 Tue Jul 28 09:19:53 2009 -0700
|
26
|
+
|
27
|
+
* Add a note about ruby-dev for debian/ubuntu users, closes #20.
|
28
|
+
* Friendlier error message on unrecognized task, closes #18, #21.
|
29
|
+
|
30
|
+
== v0.1.2 Mon Jul 27 11:46:50 2009 -0700
|
31
|
+
|
32
|
+
* Automatically include rubygems in bin/hcl.
|
33
|
+
|
34
|
+
== v0.1.1 Fri Jul 24 19:32:32 2009 -0700
|
35
|
+
|
36
|
+
* Mention gem in README, read version from file.
|
37
|
+
|
38
|
+
== v0.1.0 Fri Jul 24 19:09:23 2009 -0700
|
39
|
+
|
40
|
+
* Initial public release
|
41
|
+
|
data/LICENSE
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
Copyright (c) 2009 Zack Hobson <zack@opensourcery.com>, OpenSourcery LLC
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
4
|
+
of this software and associated documentation files (the "Software"), to deal
|
5
|
+
in the Software without restriction, including without limitation the rights
|
6
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
7
|
+
copies of the Software, and to permit persons to whom the Software is
|
8
|
+
furnished to do so, subject to the following conditions:
|
9
|
+
|
10
|
+
The above copyright notice and this permission notice shall be included in
|
11
|
+
all copies or substantial portions of the Software.
|
12
|
+
|
13
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
14
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
15
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
16
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
17
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
18
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
19
|
+
THE SOFTWARE.
|
data/README.markdown
ADDED
@@ -0,0 +1,101 @@
|
|
1
|
+
# HCl
|
2
|
+
|
3
|
+
HCl is a command-line tool for interacting with Harvest time sheets using the
|
4
|
+
[Harvest time tracking API][htt].
|
5
|
+
|
6
|
+
[htt]: http://www.getharvest.com/api/time_tracking
|
7
|
+
|
8
|
+
## Quick Start
|
9
|
+
|
10
|
+
$ gem install zenhob-hcl --source=http://gems.github.com
|
11
|
+
$ hcl show [date]
|
12
|
+
|
13
|
+
### Prerequisites
|
14
|
+
|
15
|
+
* Ruby (tested with 1.8.7)
|
16
|
+
* Ruby OpenSSL support (in debian/ubuntu: apt-get install libopenssl-ruby)
|
17
|
+
* Ruby extension building support (in debian/ubuntu: apt-get install ruby-dev)
|
18
|
+
* RubyGems
|
19
|
+
* Trollop option-parsing library (gem install trollop)
|
20
|
+
* Chronic date-parsing library (gem install chronic)
|
21
|
+
* HighLine console input library (gem install highline)
|
22
|
+
* Jeweler packaging tool (needed to build the gem)
|
23
|
+
|
24
|
+
## Usage
|
25
|
+
|
26
|
+
hcl show [date]
|
27
|
+
hcl tasks
|
28
|
+
hcl set <key> <value ...>
|
29
|
+
hcl unset <key>
|
30
|
+
hcl start (<task_alias> | <project_id> <task_id>) [+time] [msg ...]
|
31
|
+
hcl note <msg ...>
|
32
|
+
hcl stop
|
33
|
+
|
34
|
+
### Starting a Timer
|
35
|
+
|
36
|
+
To start a new timer you need to identify the project and task. After you've
|
37
|
+
used the show command you can use the tasks command to view a cached list of
|
38
|
+
available tasks. The first two numbers in each row are the project and task
|
39
|
+
IDs. You need both values to start a timer:
|
40
|
+
|
41
|
+
$ hcl show
|
42
|
+
-------------
|
43
|
+
0:00 total
|
44
|
+
$ hcl tasks
|
45
|
+
1234 5678 ClientX Software Development
|
46
|
+
1234 9876 ClientX Admin
|
47
|
+
$ hcl start 1234 5678 adding a new feature
|
48
|
+
|
49
|
+
### Task Aliases
|
50
|
+
|
51
|
+
Since it's not practical to enter two long numbers every time you want to
|
52
|
+
identify a task, HCl supports task aliases:
|
53
|
+
|
54
|
+
$ hcl set task.xdev 1234 5678
|
55
|
+
$ hcl start xdev adding a new feature
|
56
|
+
|
57
|
+
### Starting a Timer with Initial Time
|
58
|
+
|
59
|
+
You can also provide an initial time when starting a new timer.
|
60
|
+
This can be expressed in floating-point or HH:MM. The following two
|
61
|
+
commands are identical:
|
62
|
+
|
63
|
+
$ hcl start xdev +0:15 adding a new feature
|
64
|
+
$ hcl start +.25 xdev adding a new feature
|
65
|
+
|
66
|
+
### Adding Notes to a Running Task
|
67
|
+
|
68
|
+
While a task is running you can append strings to the note for that task:
|
69
|
+
|
70
|
+
$ hcl note Found a good time
|
71
|
+
$ hcl note or not, whatever...
|
72
|
+
|
73
|
+
### Stopping a Timer
|
74
|
+
|
75
|
+
The following command will stop a running timer (currently only one timer at
|
76
|
+
a time is supported):
|
77
|
+
|
78
|
+
$ hcl stop
|
79
|
+
|
80
|
+
### Date Formats
|
81
|
+
|
82
|
+
Dates can be expressed in a variety of ways. See the [Chronic documentation][cd]
|
83
|
+
for more information about available date input formats. The following
|
84
|
+
commands show the timesheet for the specified day:
|
85
|
+
|
86
|
+
$ hcl show yesterday
|
87
|
+
$ hcl show last friday
|
88
|
+
$ hcl show 2 days ago
|
89
|
+
$ hcl show 1 week ago
|
90
|
+
|
91
|
+
[cd]: http://chronic.rubyforge.org/
|
92
|
+
|
93
|
+
## Author
|
94
|
+
|
95
|
+
[Zack Hobson][zgh], [OpenSourcery LLC][os]
|
96
|
+
|
97
|
+
See LICENSE for copyright details.
|
98
|
+
|
99
|
+
[zgh]: mailto:zack@opensourcery.com
|
100
|
+
[os]: http://www.opensourcery.com/
|
101
|
+
|
data/Rakefile
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
require 'rake/testtask'
|
2
|
+
|
3
|
+
Rake::TestTask.new do |t|
|
4
|
+
t.libs << 'test'
|
5
|
+
t.test_files = FileList['test/*_test.rb']
|
6
|
+
end
|
7
|
+
|
8
|
+
begin
|
9
|
+
require 'jeweler'
|
10
|
+
Jeweler::Tasks.new do |gem|
|
11
|
+
gem.name = "hcl"
|
12
|
+
gem.summary = "Harvest timesheets from the command-line"
|
13
|
+
gem.description = "HCl is a command-line client for manipulating Harvest time sheets."
|
14
|
+
gem.email = "zack@opensourcery.com"
|
15
|
+
gem.homepage = "http://github.com/zenhob/hcl"
|
16
|
+
gem.authors = ["Zack Hobson"]
|
17
|
+
gem.add_dependency "termios"
|
18
|
+
gem.add_dependency "trollop", ">= 1.10.2"
|
19
|
+
gem.add_dependency "chronic", ">= 0.2.3"
|
20
|
+
gem.add_dependency "highline", ">= 1.5.1"
|
21
|
+
end
|
22
|
+
rescue LoadError
|
23
|
+
puts "Jeweler not available. Install it with: sudo gem install technicalpickles-jeweler -s http://gems.github.com"
|
24
|
+
end
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.2.3
|
data/bin/hcl
ADDED
data/deps.rip
ADDED
data/hcl.gemspec
ADDED
@@ -0,0 +1,75 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
|
3
|
+
Gem::Specification.new do |s|
|
4
|
+
s.name = %q{hcl}
|
5
|
+
s.version = "0.2.3"
|
6
|
+
|
7
|
+
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
8
|
+
s.authors = ["Zack Hobson"]
|
9
|
+
s.date = %q{2009-08-23}
|
10
|
+
s.default_executable = %q{hcl}
|
11
|
+
s.description = %q{HCl is a command-line client for manipulating Harvest time sheets.}
|
12
|
+
s.email = %q{zack@opensourcery.com}
|
13
|
+
s.executables = ["hcl"]
|
14
|
+
s.extra_rdoc_files = [
|
15
|
+
"LICENSE",
|
16
|
+
"README.markdown"
|
17
|
+
]
|
18
|
+
s.files = [
|
19
|
+
".document",
|
20
|
+
".gitignore",
|
21
|
+
"CHANGELOG",
|
22
|
+
"LICENSE",
|
23
|
+
"README.markdown",
|
24
|
+
"Rakefile",
|
25
|
+
"VERSION",
|
26
|
+
"bin/hcl",
|
27
|
+
"deps.rip",
|
28
|
+
"hcl.gemspec",
|
29
|
+
"lib/hcl/app.rb",
|
30
|
+
"lib/hcl/commands.rb",
|
31
|
+
"lib/hcl/day_entry.rb",
|
32
|
+
"lib/hcl/project.rb",
|
33
|
+
"lib/hcl/task.rb",
|
34
|
+
"lib/hcl/timesheet_resource.rb",
|
35
|
+
"lib/hcl/utility.rb",
|
36
|
+
"test/app_test.rb",
|
37
|
+
"test/day_entry_test.rb",
|
38
|
+
"test/test_helper.rb",
|
39
|
+
"test/utility_test.rb"
|
40
|
+
]
|
41
|
+
s.has_rdoc = true
|
42
|
+
s.homepage = %q{http://github.com/zenhob/hcl}
|
43
|
+
s.rdoc_options = ["--charset=UTF-8"]
|
44
|
+
s.require_paths = ["lib"]
|
45
|
+
s.rubygems_version = %q{1.3.1}
|
46
|
+
s.summary = %q{Harvest timesheets from the command-line}
|
47
|
+
s.test_files = [
|
48
|
+
"test/app_test.rb",
|
49
|
+
"test/day_entry_test.rb",
|
50
|
+
"test/test_helper.rb",
|
51
|
+
"test/utility_test.rb"
|
52
|
+
]
|
53
|
+
|
54
|
+
if s.respond_to? :specification_version then
|
55
|
+
current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
|
56
|
+
s.specification_version = 2
|
57
|
+
|
58
|
+
if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
|
59
|
+
s.add_runtime_dependency(%q<termios>, [">= 0"])
|
60
|
+
s.add_runtime_dependency(%q<trollop>, [">= 1.10.2"])
|
61
|
+
s.add_runtime_dependency(%q<chronic>, [">= 0.2.3"])
|
62
|
+
s.add_runtime_dependency(%q<highline>, [">= 1.5.1"])
|
63
|
+
else
|
64
|
+
s.add_dependency(%q<termios>, [">= 0"])
|
65
|
+
s.add_dependency(%q<trollop>, [">= 1.10.2"])
|
66
|
+
s.add_dependency(%q<chronic>, [">= 0.2.3"])
|
67
|
+
s.add_dependency(%q<highline>, [">= 1.5.1"])
|
68
|
+
end
|
69
|
+
else
|
70
|
+
s.add_dependency(%q<termios>, [">= 0"])
|
71
|
+
s.add_dependency(%q<trollop>, [">= 1.10.2"])
|
72
|
+
s.add_dependency(%q<chronic>, [">= 0.2.3"])
|
73
|
+
s.add_dependency(%q<highline>, [">= 1.5.1"])
|
74
|
+
end
|
75
|
+
end
|
data/lib/hcl/app.rb
ADDED
@@ -0,0 +1,164 @@
|
|
1
|
+
## stdlib dependencies
|
2
|
+
require 'yaml'
|
3
|
+
require 'rexml/document'
|
4
|
+
require 'net/http'
|
5
|
+
require 'net/https'
|
6
|
+
|
7
|
+
## gem dependencies
|
8
|
+
require 'chronic'
|
9
|
+
require 'trollop'
|
10
|
+
require 'highline/import'
|
11
|
+
|
12
|
+
## app dependencies
|
13
|
+
require 'hcl/utility'
|
14
|
+
require 'hcl/commands'
|
15
|
+
require 'hcl/timesheet_resource'
|
16
|
+
require 'hcl/project'
|
17
|
+
require 'hcl/task'
|
18
|
+
require 'hcl/day_entry'
|
19
|
+
|
20
|
+
# Workaround for annoying SSL warning:
|
21
|
+
# >> warning: peer certificate won't be verified in this SSL session
|
22
|
+
# http://www.5dollarwhitebox.org/drupal/node/64
|
23
|
+
class Net::HTTP
|
24
|
+
alias_method :old_initialize, :initialize
|
25
|
+
def initialize(*args)
|
26
|
+
old_initialize(*args)
|
27
|
+
@ssl_context = OpenSSL::SSL::SSLContext.new
|
28
|
+
@ssl_context.verify_mode = OpenSSL::SSL::VERIFY_NONE
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
module HCl
|
33
|
+
VERSION = "0.2.3"
|
34
|
+
|
35
|
+
class App
|
36
|
+
include HCl::Utility
|
37
|
+
include HCl::Commands
|
38
|
+
|
39
|
+
SETTINGS_FILE = "#{ENV['HOME']}/.hcl_settings"
|
40
|
+
CONFIG_FILE = "#{ENV['HOME']}/.hcl_config"
|
41
|
+
|
42
|
+
class UnknownCommand < StandardError; end
|
43
|
+
|
44
|
+
def initialize
|
45
|
+
read_config
|
46
|
+
read_settings
|
47
|
+
end
|
48
|
+
|
49
|
+
# Run the given command and arguments.
|
50
|
+
def self.command *args
|
51
|
+
hcl = new.process_args(*args).run
|
52
|
+
end
|
53
|
+
|
54
|
+
# Return true if the string is a known command, false otherwise.
|
55
|
+
#
|
56
|
+
# @param [#to_s] command name of command
|
57
|
+
# @return [true, false]
|
58
|
+
def command? command
|
59
|
+
Commands.instance_methods.include? command.to_s
|
60
|
+
end
|
61
|
+
|
62
|
+
# Start the application.
|
63
|
+
def run
|
64
|
+
begin
|
65
|
+
if @command
|
66
|
+
if command? @command
|
67
|
+
result = send @command, *@args
|
68
|
+
if not result.nil?
|
69
|
+
if result.respond_to? :to_a
|
70
|
+
puts result.to_a.join(', ')
|
71
|
+
elsif result.respond_to? :to_s
|
72
|
+
puts result
|
73
|
+
end
|
74
|
+
end
|
75
|
+
else
|
76
|
+
raise UnknownCommand, "unrecognized command `#{@command}'"
|
77
|
+
end
|
78
|
+
else
|
79
|
+
show
|
80
|
+
end
|
81
|
+
rescue TimesheetResource::Failure => e
|
82
|
+
puts "Internal failure. #{e}"
|
83
|
+
exit 1
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
def process_args *args
|
88
|
+
Trollop::options(args) do
|
89
|
+
stop_on Commands.instance_methods
|
90
|
+
version "HCl version #{VERSION}"
|
91
|
+
banner <<-EOM
|
92
|
+
HCl is a command-line client for manipulating Harvest time sheets.
|
93
|
+
|
94
|
+
Commands:
|
95
|
+
hcl show [date]
|
96
|
+
hcl tasks
|
97
|
+
hcl aliases
|
98
|
+
hcl set <key> <value ...>
|
99
|
+
hcl unset <key>
|
100
|
+
hcl start <task> [msg]
|
101
|
+
hcl stop [msg]
|
102
|
+
hcl note <msg>
|
103
|
+
|
104
|
+
Examples:
|
105
|
+
$ hcl tasks
|
106
|
+
$ hcl start 1234 4567 this is my log message
|
107
|
+
$ hcl set task.mytask 1234 4567
|
108
|
+
$ hcl start mytask this is my next log message
|
109
|
+
$ hcl show yesterday
|
110
|
+
$ hcl show last tuesday
|
111
|
+
|
112
|
+
Options:
|
113
|
+
EOM
|
114
|
+
end
|
115
|
+
@command = args.shift
|
116
|
+
@args = args
|
117
|
+
self
|
118
|
+
end
|
119
|
+
|
120
|
+
protected
|
121
|
+
|
122
|
+
def read_config
|
123
|
+
if File.exists? CONFIG_FILE
|
124
|
+
config = YAML::load File.read(CONFIG_FILE)
|
125
|
+
TimesheetResource.configure config
|
126
|
+
elsif File.exists? old_conf = File.dirname(__FILE__) + "/../hcl_conf.yml"
|
127
|
+
config = YAML::load File.read(old_conf)
|
128
|
+
TimesheetResource.configure config
|
129
|
+
write_config config
|
130
|
+
else
|
131
|
+
config = {}
|
132
|
+
puts "Please specify your Harvest credentials.\n"
|
133
|
+
config['login'] = ask("Email Address: ")
|
134
|
+
config['password'] = ask("Password: ") { |q| q.echo = false }
|
135
|
+
config['subdomain'] = ask("Subdomain: ")
|
136
|
+
TimesheetResource.configure config
|
137
|
+
write_config config
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
def write_config config
|
142
|
+
puts "Writing configuration to #{CONFIG_FILE}."
|
143
|
+
File.open(CONFIG_FILE, 'w') do |f|
|
144
|
+
f.write config.to_yaml
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
def read_settings
|
149
|
+
if File.exists? SETTINGS_FILE
|
150
|
+
@settings = YAML.load(File.read(SETTINGS_FILE))
|
151
|
+
else
|
152
|
+
@settings = {}
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
def write_settings
|
157
|
+
File.open(SETTINGS_FILE, 'w') do |f|
|
158
|
+
f.write @settings.to_yaml
|
159
|
+
end
|
160
|
+
nil
|
161
|
+
end
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
data/lib/hcl/commands.rb
ADDED
@@ -0,0 +1,90 @@
|
|
1
|
+
module HCl
|
2
|
+
module Commands
|
3
|
+
def tasks
|
4
|
+
tasks = Task.all
|
5
|
+
if tasks.empty?
|
6
|
+
puts "No cached tasks. Run `hcl show' to populate the cache and try again."
|
7
|
+
else
|
8
|
+
tasks.each { |task| puts "#{task.project.id} #{task.id}\t#{task}" }
|
9
|
+
end
|
10
|
+
nil
|
11
|
+
end
|
12
|
+
|
13
|
+
def set key = nil, *args
|
14
|
+
if key.nil?
|
15
|
+
@settings.each do |k, v|
|
16
|
+
puts "#{k}: #{v}"
|
17
|
+
end
|
18
|
+
else
|
19
|
+
value = args.join(' ')
|
20
|
+
@settings ||= {}
|
21
|
+
@settings[key] = value
|
22
|
+
write_settings
|
23
|
+
end
|
24
|
+
nil
|
25
|
+
end
|
26
|
+
|
27
|
+
def unset key
|
28
|
+
@settings.delete key
|
29
|
+
write_settings
|
30
|
+
end
|
31
|
+
|
32
|
+
def aliases
|
33
|
+
@settings.keys.select { |s| s =~ /^task\./ }.map { |s| s.slice(5..-1) }
|
34
|
+
end
|
35
|
+
|
36
|
+
def start *args
|
37
|
+
starting_time = args.detect {|x| x =~ /^\+\d*(\.|:)?\d+$/ }
|
38
|
+
if starting_time
|
39
|
+
args.delete(starting_time)
|
40
|
+
starting_time = time2float starting_time
|
41
|
+
end
|
42
|
+
ident = args.shift
|
43
|
+
task_ids = if @settings.key? "task.#{ident}"
|
44
|
+
@settings["task.#{ident}"].split(/\s+/)
|
45
|
+
else
|
46
|
+
[ident, args.shift]
|
47
|
+
end
|
48
|
+
task = Task.find *task_ids
|
49
|
+
if task.nil?
|
50
|
+
puts "Unknown project/task alias, try one of the following: #{aliases.join(', ')}."
|
51
|
+
exit 1
|
52
|
+
end
|
53
|
+
timer = task.start(:starting_time => starting_time, :note => args.join(' '))
|
54
|
+
puts "Started timer for #{timer}."
|
55
|
+
end
|
56
|
+
|
57
|
+
def stop
|
58
|
+
entry = DayEntry.with_timer
|
59
|
+
if entry
|
60
|
+
entry.toggle
|
61
|
+
puts "Stopped #{entry}."
|
62
|
+
else
|
63
|
+
puts "No running timers found."
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def note *args
|
68
|
+
message = args.join ' '
|
69
|
+
entry = DayEntry.with_timer
|
70
|
+
if entry
|
71
|
+
entry.append_note message
|
72
|
+
puts "Added note '#{message}' to #{entry}."
|
73
|
+
else
|
74
|
+
puts "No running timers found."
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
def show *args
|
79
|
+
date = args.empty? ? nil : Chronic.parse(args.join(' '))
|
80
|
+
total_hours = 0.0
|
81
|
+
DayEntry.all(date).each do |day|
|
82
|
+
running = day.running? ? '(running) ' : ''
|
83
|
+
puts "\t#{day.formatted_hours}\t#{running}#{day.project} #{day.notes}"[0..78]
|
84
|
+
total_hours = total_hours + day.hours.to_f
|
85
|
+
end
|
86
|
+
puts "\t" + '-' * 13
|
87
|
+
puts "\t#{as_hours total_hours}\ttotal"
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
|
2
|
+
module HCl
|
3
|
+
class DayEntry < TimesheetResource
|
4
|
+
include Utility
|
5
|
+
|
6
|
+
# Get the time sheet entries for a given day. If no date is provided
|
7
|
+
# defaults to today.
|
8
|
+
def self.all date = nil
|
9
|
+
url = date.nil? ? 'daily' : "daily/#{date.strftime '%j/%Y'}"
|
10
|
+
from_xml get(url)
|
11
|
+
end
|
12
|
+
|
13
|
+
def to_s
|
14
|
+
"#{client} #{project} #{task} (#{formatted_hours})"
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.from_xml xml
|
18
|
+
doc = REXML::Document.new xml
|
19
|
+
raise Failure, "No root node in XML document: #{xml}" if doc.root.nil?
|
20
|
+
Task.cache_tasks doc
|
21
|
+
doc.root.elements.collect('//day_entry') do |day|
|
22
|
+
new xml_to_hash(day)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def notes
|
27
|
+
super || @data[:notes] = ''
|
28
|
+
end
|
29
|
+
|
30
|
+
# Append a string to the notes for this task.
|
31
|
+
def append_note new_notes
|
32
|
+
# If I don't include hours it gets reset.
|
33
|
+
# This doens't appear to be the case for task and project.
|
34
|
+
DayEntry.post("daily/update/#{id}", <<-EOD)
|
35
|
+
<request>
|
36
|
+
<notes>#{notes << " #{new_notes}"}</notes>
|
37
|
+
<hours>#{hours}</hours>
|
38
|
+
</request>
|
39
|
+
EOD
|
40
|
+
end
|
41
|
+
|
42
|
+
def self.with_timer
|
43
|
+
all.detect {|t| t.running? }
|
44
|
+
end
|
45
|
+
|
46
|
+
def running?
|
47
|
+
!@data[:timer_started_at].nil? && !@data[:timer_started_at].empty?
|
48
|
+
end
|
49
|
+
|
50
|
+
def initialize *args
|
51
|
+
super
|
52
|
+
# TODO cache client/project names and ids
|
53
|
+
end
|
54
|
+
|
55
|
+
def toggle
|
56
|
+
DayEntry.get("daily/timer/#{id}")
|
57
|
+
self
|
58
|
+
end
|
59
|
+
|
60
|
+
# Returns the hours formatted as "HH:MM"
|
61
|
+
def formatted_hours
|
62
|
+
as_hours hours
|
63
|
+
end
|
64
|
+
|
65
|
+
end
|
66
|
+
end
|
data/lib/hcl/project.rb
ADDED
data/lib/hcl/task.rb
ADDED
@@ -0,0 +1,58 @@
|
|
1
|
+
module HCl
|
2
|
+
class Task < TimesheetResource
|
3
|
+
def self.cache_tasks doc
|
4
|
+
tasks = []
|
5
|
+
doc.root.elements.collect('projects/project') do |project_elem|
|
6
|
+
project = Project.new xml_to_hash(project_elem)
|
7
|
+
tasks.concat(project_elem.elements.collect('tasks/task') do |task|
|
8
|
+
new xml_to_hash(task).merge(:project => project)
|
9
|
+
end)
|
10
|
+
end
|
11
|
+
unless tasks.empty?
|
12
|
+
File.open(File.join(ENV['HOME'],'.hcl_tasks'), 'w') do |f|
|
13
|
+
f.write tasks.uniq.to_yaml
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.all
|
19
|
+
YAML.load File.read(File.join(ENV['HOME'],'.hcl_tasks'))
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.find project_id, id
|
23
|
+
all.detect do |t|
|
24
|
+
t.project.id.to_i == project_id.to_i && t.id.to_i == id.to_i
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def to_s
|
29
|
+
"#{project.name} #{name}"
|
30
|
+
end
|
31
|
+
|
32
|
+
def add opts
|
33
|
+
notes = opts[:note]
|
34
|
+
starting_time = opts[:starting_time] || 0
|
35
|
+
days = DayEntry.from_xml Task.post("daily/add", <<-EOT)
|
36
|
+
<request>
|
37
|
+
<notes>#{notes}</notes>
|
38
|
+
<hours>#{starting_time}</hours>
|
39
|
+
<project_id type="integer">#{project.id}</project_id>
|
40
|
+
<task_id type="integer">#{id}</task_id>
|
41
|
+
<spent_at type="date">#{Date.today}</spent_at>
|
42
|
+
</request>
|
43
|
+
EOT
|
44
|
+
days.first
|
45
|
+
end
|
46
|
+
|
47
|
+
def start opts
|
48
|
+
day = add opts
|
49
|
+
if day.running?
|
50
|
+
day
|
51
|
+
else
|
52
|
+
DayEntry.from_xml(Task.get("daily/timer/#{day.id}")).first
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
@@ -0,0 +1,74 @@
|
|
1
|
+
module HCl
|
2
|
+
class TimesheetResource
|
3
|
+
class Failure < Exception; end
|
4
|
+
|
5
|
+
def self.configure opts = nil
|
6
|
+
if opts
|
7
|
+
self.login = opts['login']
|
8
|
+
self.password = opts['password']
|
9
|
+
self.subdomain = opts['subdomain']
|
10
|
+
else
|
11
|
+
yield self
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
# configuration accessors
|
16
|
+
%w[ login password subdomain ].each do |config_var|
|
17
|
+
class_eval <<-EOC
|
18
|
+
def self.#{config_var}= arg
|
19
|
+
@@#{config_var} = arg
|
20
|
+
end
|
21
|
+
def self.#{config_var}
|
22
|
+
@@#{config_var}
|
23
|
+
end
|
24
|
+
EOC
|
25
|
+
end
|
26
|
+
|
27
|
+
def initialize params
|
28
|
+
@data = params
|
29
|
+
end
|
30
|
+
|
31
|
+
def self.get action
|
32
|
+
https_do Net::HTTP::Get, action
|
33
|
+
end
|
34
|
+
|
35
|
+
def self.post action, data
|
36
|
+
https_do Net::HTTP::Post, action, data
|
37
|
+
end
|
38
|
+
|
39
|
+
def self.https_do method_class, action, data = nil
|
40
|
+
https = Net::HTTP.new "#{subdomain}.harvestapp.com", 443
|
41
|
+
request = method_class.new "/#{action}"
|
42
|
+
https.use_ssl = true
|
43
|
+
request.basic_auth login, password
|
44
|
+
request.content_type = 'application/xml'
|
45
|
+
request['Accept'] = 'application/xml'
|
46
|
+
response = https.request request, data
|
47
|
+
return response.body
|
48
|
+
if response.kind_of? Net::HTTPSuccess
|
49
|
+
response.body
|
50
|
+
else
|
51
|
+
raise 'failure'
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def id
|
56
|
+
@data[:id]
|
57
|
+
end
|
58
|
+
|
59
|
+
def method_missing method, *args
|
60
|
+
if @data.key? method.to_sym
|
61
|
+
@data[method]
|
62
|
+
else
|
63
|
+
super
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def self.xml_to_hash elem
|
68
|
+
elem.elements.map { |e| e.name }.inject({}) do |a, f|
|
69
|
+
a[f.to_sym] = elem.elements[f].text if elem.elements[f]
|
70
|
+
a
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
data/lib/hcl/utility.rb
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
module HCl
|
2
|
+
module Utility
|
3
|
+
# Convert from decimal to a string of the form HH:MM.
|
4
|
+
#
|
5
|
+
# @param [#to_f] hours number of hours in decimal
|
6
|
+
# @return [String] of the form "HH:MM"
|
7
|
+
def as_hours hours
|
8
|
+
minutes = hours.to_f * 60.0
|
9
|
+
sprintf "%d:%02d", (minutes / 60).to_i, (minutes % 60).to_i
|
10
|
+
end
|
11
|
+
|
12
|
+
# Convert from a time span in hour or decimal format to a float.
|
13
|
+
#
|
14
|
+
# @param [String] time_string either "M:MM" or decimal
|
15
|
+
# @return [#to_f] converted to a floating-point number
|
16
|
+
def time2float time_string
|
17
|
+
if time_string =~ /:/
|
18
|
+
hours, minutes = time_string.split(':')
|
19
|
+
hours.to_f + (minutes.to_f / 60.0)
|
20
|
+
else
|
21
|
+
time_string.to_f
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
data/test/app_test.rb
ADDED
@@ -0,0 +1,49 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
class DayEntryTest < Test::Unit::TestCase
|
4
|
+
should "read DayEntry xml" do
|
5
|
+
entries = HCl::DayEntry.from_xml(<<-EOD)
|
6
|
+
<daily>
|
7
|
+
<for_day type="date">Wed, 18 Oct 2006</for_day>
|
8
|
+
<day_entries>
|
9
|
+
<day_entry>
|
10
|
+
<id type="integer">195168</id>
|
11
|
+
<client>Iridesco</client>
|
12
|
+
<project>Harvest</project>
|
13
|
+
<task>Backend Programming</task>
|
14
|
+
<hours type="float">2.06</hours>
|
15
|
+
<notes>Test api support</notes>
|
16
|
+
<timer_started_at type="datetime">
|
17
|
+
Wed, 18 Oct 2006 09:53:06 -0000
|
18
|
+
</timer_started_at>
|
19
|
+
<created_at type="datetime">Wed, 18 Oct 2006 09:53:06 -0000</created_at>
|
20
|
+
</day_entry>
|
21
|
+
</day_entries>
|
22
|
+
</daily>
|
23
|
+
EOD
|
24
|
+
assert_equal 1, entries.size
|
25
|
+
{
|
26
|
+
:project => 'Harvest',
|
27
|
+
:client => 'Iridesco',
|
28
|
+
:task => 'Backend Programming',
|
29
|
+
:notes => 'Test api support',
|
30
|
+
:hours => '2.06',
|
31
|
+
}.each do |method, value|
|
32
|
+
assert_equal value, entries.first.send(method)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
should "append to an existing note" do
|
37
|
+
entry = HCl::DayEntry.new(:id => '1', :notes => 'yourmom.', :hours => '1.0')
|
38
|
+
HCl::DayEntry.stubs(:post)
|
39
|
+
entry.append_note('hi world')
|
40
|
+
assert_equal 'yourmom. hi world', entry.notes
|
41
|
+
end
|
42
|
+
|
43
|
+
should "append to an undefined note" do
|
44
|
+
entry = HCl::DayEntry.new(:id => '1', :notes => nil, :hours => '1.0')
|
45
|
+
HCl::DayEntry.stubs(:post)
|
46
|
+
entry.append_note('hi world')
|
47
|
+
assert_equal ' hi world', entry.notes
|
48
|
+
end
|
49
|
+
end
|
data/test/test_helper.rb
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
class UtilityTest < Test::Unit::TestCase
|
4
|
+
include HCl::Utility
|
5
|
+
|
6
|
+
should "convert decimal input when converting time2float" do
|
7
|
+
assert_equal 2.5, time2float("2.5")
|
8
|
+
end
|
9
|
+
|
10
|
+
should "convert HH:MM input when converting time2float" do
|
11
|
+
assert_equal 2.5, time2float("2:30")
|
12
|
+
end
|
13
|
+
|
14
|
+
should "assume decimal input when converting time2float" do
|
15
|
+
assert_equal 2.0, time2float("2")
|
16
|
+
end
|
17
|
+
end
|
metadata
ADDED
@@ -0,0 +1,118 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: hcl
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.2.3
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Zack Hobson
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2009-08-23 00:00:00 -07:00
|
13
|
+
default_executable: hcl
|
14
|
+
dependencies:
|
15
|
+
- !ruby/object:Gem::Dependency
|
16
|
+
name: termios
|
17
|
+
type: :runtime
|
18
|
+
version_requirement:
|
19
|
+
version_requirements: !ruby/object:Gem::Requirement
|
20
|
+
requirements:
|
21
|
+
- - ">="
|
22
|
+
- !ruby/object:Gem::Version
|
23
|
+
version: "0"
|
24
|
+
version:
|
25
|
+
- !ruby/object:Gem::Dependency
|
26
|
+
name: trollop
|
27
|
+
type: :runtime
|
28
|
+
version_requirement:
|
29
|
+
version_requirements: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: 1.10.2
|
34
|
+
version:
|
35
|
+
- !ruby/object:Gem::Dependency
|
36
|
+
name: chronic
|
37
|
+
type: :runtime
|
38
|
+
version_requirement:
|
39
|
+
version_requirements: !ruby/object:Gem::Requirement
|
40
|
+
requirements:
|
41
|
+
- - ">="
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
version: 0.2.3
|
44
|
+
version:
|
45
|
+
- !ruby/object:Gem::Dependency
|
46
|
+
name: highline
|
47
|
+
type: :runtime
|
48
|
+
version_requirement:
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
50
|
+
requirements:
|
51
|
+
- - ">="
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: 1.5.1
|
54
|
+
version:
|
55
|
+
description: HCl is a command-line client for manipulating Harvest time sheets.
|
56
|
+
email: zack@opensourcery.com
|
57
|
+
executables:
|
58
|
+
- hcl
|
59
|
+
extensions: []
|
60
|
+
|
61
|
+
extra_rdoc_files:
|
62
|
+
- LICENSE
|
63
|
+
- README.markdown
|
64
|
+
files:
|
65
|
+
- .document
|
66
|
+
- .gitignore
|
67
|
+
- CHANGELOG
|
68
|
+
- LICENSE
|
69
|
+
- README.markdown
|
70
|
+
- Rakefile
|
71
|
+
- VERSION
|
72
|
+
- bin/hcl
|
73
|
+
- deps.rip
|
74
|
+
- hcl.gemspec
|
75
|
+
- lib/hcl/app.rb
|
76
|
+
- lib/hcl/commands.rb
|
77
|
+
- lib/hcl/day_entry.rb
|
78
|
+
- lib/hcl/project.rb
|
79
|
+
- lib/hcl/task.rb
|
80
|
+
- lib/hcl/timesheet_resource.rb
|
81
|
+
- lib/hcl/utility.rb
|
82
|
+
- test/app_test.rb
|
83
|
+
- test/day_entry_test.rb
|
84
|
+
- test/test_helper.rb
|
85
|
+
- test/utility_test.rb
|
86
|
+
has_rdoc: true
|
87
|
+
homepage: http://github.com/zenhob/hcl
|
88
|
+
licenses: []
|
89
|
+
|
90
|
+
post_install_message:
|
91
|
+
rdoc_options:
|
92
|
+
- --charset=UTF-8
|
93
|
+
require_paths:
|
94
|
+
- lib
|
95
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
96
|
+
requirements:
|
97
|
+
- - ">="
|
98
|
+
- !ruby/object:Gem::Version
|
99
|
+
version: "0"
|
100
|
+
version:
|
101
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
102
|
+
requirements:
|
103
|
+
- - ">="
|
104
|
+
- !ruby/object:Gem::Version
|
105
|
+
version: "0"
|
106
|
+
version:
|
107
|
+
requirements: []
|
108
|
+
|
109
|
+
rubyforge_project:
|
110
|
+
rubygems_version: 1.3.5
|
111
|
+
signing_key:
|
112
|
+
specification_version: 2
|
113
|
+
summary: Harvest timesheets from the command-line
|
114
|
+
test_files:
|
115
|
+
- test/app_test.rb
|
116
|
+
- test/day_entry_test.rb
|
117
|
+
- test/test_helper.rb
|
118
|
+
- test/utility_test.rb
|