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.
@@ -0,0 +1 @@
1
+ --files CHANGELOG
@@ -0,0 +1,5 @@
1
+ *.sw[nop]
2
+ pkg
3
+ tags
4
+ doc
5
+ .yardoc
@@ -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.
@@ -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
+
@@ -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
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'hcl/app'
4
+
5
+ HCl::App.command *ARGV
6
+
@@ -0,0 +1,3 @@
1
+ git://github.com/lutzky/trollop.git a8d63c816 # 1.10.2
2
+ git://github.com/mojombo/chronic.git 180e44763 # 0.3.0
3
+ git://github.com/JEG2/highline.git rel_1_5_1
@@ -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
@@ -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
+
@@ -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
@@ -0,0 +1,3 @@
1
+ class HCl::Project < HCl::TimesheetResource
2
+ end
3
+
@@ -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
@@ -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
@@ -0,0 +1,8 @@
1
+ require 'test_helper'
2
+ class AppTest < Test::Unit::TestCase
3
+
4
+ should "permit commands from the HCl::Commands module" do
5
+ app = HCl::App.new
6
+ assert HCl::Commands.instance_methods.all? { |c| app.command? c }
7
+ end
8
+ end
@@ -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
@@ -0,0 +1,6 @@
1
+ $:.unshift(File.dirname(__FILE__) + '/../lib')
2
+
3
+ require 'test/unit'
4
+ require 'hcl/app'
5
+ require 'shoulda'
6
+ require 'mocha'
@@ -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