harvest 0.8.2
Sign up to get free protection for your applications and to get access to all the features.
- data/HISTORY +3 -0
- data/LICENSE +23 -0
- data/README.rdoc +173 -0
- data/Rakefile +44 -0
- data/lib/harvest.rb +35 -0
- data/lib/harvest/base.rb +77 -0
- data/lib/harvest/harvest_resource.rb +15 -0
- data/lib/harvest/plugins/active_resource_inheritable_headers.rb +36 -0
- data/lib/harvest/plugins/toggleable.rb +12 -0
- data/lib/harvest/resources/client.rb +8 -0
- data/lib/harvest/resources/entry.rb +34 -0
- data/lib/harvest/resources/expense.rb +22 -0
- data/lib/harvest/resources/expense_category.rb +7 -0
- data/lib/harvest/resources/person.rb +44 -0
- data/lib/harvest/resources/project.rb +51 -0
- data/lib/harvest/resources/task.rb +8 -0
- data/lib/harvest/resources/task_assignment.rb +27 -0
- data/lib/harvest/resources/user_assignment.rb +27 -0
- data/test/integration/client_integration.rb +17 -0
- data/test/integration/client_teardown.rb +11 -0
- data/test/integration/expense_category_integration.rb +16 -0
- data/test/integration/expense_category_teardown.rb +12 -0
- data/test/integration/harvest_integration_test.rb +47 -0
- data/test/integration/project_integration.rb +19 -0
- data/test/integration/project_teardown.rb +12 -0
- data/test/integration/task_integration.rb +19 -0
- data/test/integration/task_teardown.rb +12 -0
- data/test/test_helper.rb +51 -0
- data/test/unit/base_test.rb +88 -0
- data/test/unit/resources/client_test.rb +82 -0
- data/test/unit/resources/expense_category_test.rb +49 -0
- data/test/unit/resources/expense_test.rb +14 -0
- data/test/unit/resources/person_test.rb +150 -0
- data/test/unit/resources/project_test.rb +154 -0
- data/test/unit/resources/task_assignment_test.rb +72 -0
- data/test/unit/resources/task_test.rb +82 -0
- data/test/unit/resources/user_assignment_test.rb +71 -0
- metadata +111 -0
data/HISTORY
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
Copyright (c) 2008, Kyle Banker, Alexander Interactive, Inc.
|
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
|
+
Except as contained in this notice, the name(s) of the above copyright holders
|
14
|
+
shall not be used in advertising or otherwise to promote the sale, use or other
|
15
|
+
dealings in this Software without prior written authorization.
|
16
|
+
|
17
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
18
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
19
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
20
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
21
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
22
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
23
|
+
THE SOFTWARE.
|
data/README.rdoc
ADDED
@@ -0,0 +1,173 @@
|
|
1
|
+
= harvest
|
2
|
+
|
3
|
+
A ruby library wrapping (most of) the Harvest Api.
|
4
|
+
|
5
|
+
== DESCRIPTION:
|
6
|
+
|
7
|
+
This library uses ActiveResource and activeresource_throttle to provide simple, reliable access to the Harvest Api. Good for building reporting scripts and simple apps.
|
8
|
+
|
9
|
+
Note that this library does not support the complete Harvest Api at this time. See Features/Issues below for details.
|
10
|
+
|
11
|
+
== USAGE:
|
12
|
+
|
13
|
+
=== Creating a new harvest object:
|
14
|
+
|
15
|
+
All calls to the Harvest API will originate from a Harvest object. Initialize it like so:
|
16
|
+
|
17
|
+
@harvest = Harvest(:email => "joe@example.com",
|
18
|
+
:password => "secret",
|
19
|
+
:sub_domain => "joeandcompany",
|
20
|
+
:headers => {"User-Agent" => "MyCompany"})
|
21
|
+
|
22
|
+
The _headers_ argument is optional. Use <b>:ssl => true</b> if your account requires HTTPS.
|
23
|
+
|
24
|
+
=== Clients
|
25
|
+
|
26
|
+
Index
|
27
|
+
@harvest.clients.find(:all)
|
28
|
+
Show
|
29
|
+
@harvest.clients.find(43235)
|
30
|
+
Create
|
31
|
+
@client = @harvest.clients.new
|
32
|
+
@client.attributes = {:name => "Company, LLC", :details => "New York, NY"}
|
33
|
+
@client.save
|
34
|
+
Update
|
35
|
+
@client.first_name = "Smith"
|
36
|
+
@client.save
|
37
|
+
Destroy
|
38
|
+
@client.destroy
|
39
|
+
|
40
|
+
Toggle (Active/Inactive)
|
41
|
+
@client.toggle
|
42
|
+
|
43
|
+
=== Tasks
|
44
|
+
|
45
|
+
Index
|
46
|
+
@harvest.tasks.find(:all)
|
47
|
+
Show
|
48
|
+
@harvest.tasks.find(123)
|
49
|
+
Create
|
50
|
+
@task = @harvest.tasks.new
|
51
|
+
@task.attributes = {:name => "Meeting", :billable_by_default => false,
|
52
|
+
:is_default => false, :default_hourly_rate => 100}
|
53
|
+
@task.save
|
54
|
+
Update
|
55
|
+
@task.name = "Client Meeting"
|
56
|
+
@task.save
|
57
|
+
Destroy
|
58
|
+
@task.destroy
|
59
|
+
|
60
|
+
=== People
|
61
|
+
|
62
|
+
Index
|
63
|
+
@harvest.people.find(:all)
|
64
|
+
Show
|
65
|
+
@harvest.people.find(123)
|
66
|
+
|
67
|
+
=== Projects
|
68
|
+
|
69
|
+
Index
|
70
|
+
@harvest.projects.find(:all)
|
71
|
+
Show
|
72
|
+
@harvest.projects.find(123)
|
73
|
+
Create
|
74
|
+
@project = @harvest.projects.new
|
75
|
+
@project.attributes = {:name => "Refactor", :active => true,
|
76
|
+
:bill_by => "None", :client_id => @client.id, }
|
77
|
+
@project.save
|
78
|
+
Update
|
79
|
+
@project.name = "Mega-Refactor"
|
80
|
+
@project.save
|
81
|
+
Destroy
|
82
|
+
@project.destroy
|
83
|
+
|
84
|
+
Toggle (Active/Inactive)
|
85
|
+
@project.toggle
|
86
|
+
|
87
|
+
=== Invoices
|
88
|
+
|
89
|
+
Index
|
90
|
+
@invoices = @harvest.invoices.find(:all)
|
91
|
+
|
92
|
+
Show
|
93
|
+
@invoice = @harvest.invoices.find(1)
|
94
|
+
|
95
|
+
Find By Invoice Number
|
96
|
+
@invoice = @harvest.invoices.find_by_number('200')
|
97
|
+
|
98
|
+
Create
|
99
|
+
@invoice = @harvest.invoices.new
|
100
|
+
@invoice.attributes = {:amount => '250.32', :client_id => @client.id}
|
101
|
+
@invoice.save
|
102
|
+
|
103
|
+
Update
|
104
|
+
@invoice.notes = "Pending payment"
|
105
|
+
@invoice.save
|
106
|
+
|
107
|
+
Destroy
|
108
|
+
@invoice.destroy
|
109
|
+
|
110
|
+
Return parsed csv line items
|
111
|
+
@invoice.parsed_csv_line_items
|
112
|
+
|
113
|
+
==== User and Task Assignments
|
114
|
+
|
115
|
+
@project.users.find(:all)
|
116
|
+
@project.users.find(:first)
|
117
|
+
|
118
|
+
@project.tasks.find(:all)
|
119
|
+
@project.tasks.find(:first)
|
120
|
+
|
121
|
+
=== Reports
|
122
|
+
|
123
|
+
@project.entries(:from => Time.now, :to => Time.now)
|
124
|
+
@project.entries(:from => Time.now, :to => Time.now, :user_id => @person.id)
|
125
|
+
|
126
|
+
@person.entries(:from => Time.now, :to => Time.now)
|
127
|
+
@person.expenses(:from => Time.now, :to => Time.now)
|
128
|
+
|
129
|
+
|
130
|
+
== FEATURES/PROBLEMS:
|
131
|
+
|
132
|
+
Supports most of the Harvest Api (http://www.getharvest.com/api).
|
133
|
+
|
134
|
+
Though RESTful, the Harvest Api does not entirely follow ActiveResource conventions. In order to support the complete API, certain customizations will be required. <b>We welcome any contributions/modifications to help complete this Api.</b>
|
135
|
+
|
136
|
+
The following Api features are incomplete due to incompatibilities with ActiveResource conventions (CRUD actions in parentheses):
|
137
|
+
|
138
|
+
1. People (CUD)
|
139
|
+
2. User & Task Assignments (CUD)
|
140
|
+
3. Expenses (CUD)
|
141
|
+
|
142
|
+
In addition, the following have not been implemented at all due to time constraints:
|
143
|
+
|
144
|
+
1. Time Tracking Api
|
145
|
+
2. Invoices, Invoice Payments, Invoice Categories, and Invoice Messages
|
146
|
+
|
147
|
+
Expect a more complete library in the near future. Again, contributions are welcome.
|
148
|
+
|
149
|
+
== TEST SUITE
|
150
|
+
|
151
|
+
This library includes a fairly thorough set of unit and integration tests. The unit tests can be run with the "rake" command.
|
152
|
+
|
153
|
+
The integration tests require a Harvest account. It is best to use a trial account when running these tests.
|
154
|
+
|
155
|
+
The credentials may be entered in test/test_helper.rb. To run the integration suite:
|
156
|
+
|
157
|
+
rake test:integration
|
158
|
+
|
159
|
+
Note that the integration test structure is somewhat unorthodox, as it requires tests to run in a particular order. The main test file is test/integration/harvest_integration_test.rb. Examine this to see how the integration suite works.
|
160
|
+
|
161
|
+
== REQUIREMENTS:
|
162
|
+
|
163
|
+
Requires active_resource >= 2.1 and activeresource_throttle >= 1.0.
|
164
|
+
|
165
|
+
active_resource_throttle should install automaticallt, but just in case you need to install manually:
|
166
|
+
|
167
|
+
gem sources -a http://gems.github.com
|
168
|
+
sudo gem install aiaio-active_resource_throttle
|
169
|
+
|
170
|
+
== INSTALL:
|
171
|
+
|
172
|
+
gem sources -a http://gems.github.com
|
173
|
+
gem install aiaio-harvest
|
data/Rakefile
ADDED
@@ -0,0 +1,44 @@
|
|
1
|
+
# -*- ruby -*-
|
2
|
+
|
3
|
+
require 'rubygems'
|
4
|
+
require 'rake'
|
5
|
+
require 'rake/testtask'
|
6
|
+
require './lib/harvest.rb'
|
7
|
+
|
8
|
+
desc 'Default: run all tests.'
|
9
|
+
task :default => :test_all
|
10
|
+
|
11
|
+
namespace :test do
|
12
|
+
|
13
|
+
Rake::TestTask.new(:resources) do |t|
|
14
|
+
t.libs << 'lib'
|
15
|
+
t.pattern = 'test/unit/resources/*_test.rb'
|
16
|
+
t.verbose = false
|
17
|
+
end
|
18
|
+
|
19
|
+
Rake::TestTask.new(:base) do |t|
|
20
|
+
t.libs << 'lib'
|
21
|
+
t.pattern = 'test/unit/*_test.rb'
|
22
|
+
t.verbose = false
|
23
|
+
end
|
24
|
+
|
25
|
+
Rake::TestTask.new(:integration) do |t|
|
26
|
+
t.libs << 'lib'
|
27
|
+
t.pattern = 'test/integration/*_test.rb'
|
28
|
+
t.verbose = false
|
29
|
+
end
|
30
|
+
|
31
|
+
end
|
32
|
+
|
33
|
+
desc "Test everything."
|
34
|
+
task :test_all do
|
35
|
+
errors = %w(test:resources test:base).collect do |task|
|
36
|
+
begin
|
37
|
+
Rake::Task[task].invoke
|
38
|
+
nil
|
39
|
+
rescue => e
|
40
|
+
task
|
41
|
+
end
|
42
|
+
end.compact
|
43
|
+
abort "Errors running #{errors.to_sentence}!" if errors.any?
|
44
|
+
end
|
data/lib/harvest.rb
ADDED
@@ -0,0 +1,35 @@
|
|
1
|
+
module Harvest
|
2
|
+
VERSION = "0.8"
|
3
|
+
ApiDomain = "harvestapp.com"
|
4
|
+
|
5
|
+
# Class method to load all ruby files from a given path.
|
6
|
+
def self.load_all_ruby_files_from_path(path)
|
7
|
+
Dir.foreach(path) do |file|
|
8
|
+
require File.join(path, file) if file =~ /\.rb$/
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
end
|
13
|
+
|
14
|
+
# Gems
|
15
|
+
require "activeresource"
|
16
|
+
require "active_resource_throttle"
|
17
|
+
|
18
|
+
# Plugins
|
19
|
+
PluginPath = File.join(File.dirname(__FILE__), "harvest", "plugins")
|
20
|
+
Harvest.load_all_ruby_files_from_path(PluginPath)
|
21
|
+
|
22
|
+
# Base
|
23
|
+
require File.join(File.dirname(__FILE__), "harvest", "base")
|
24
|
+
require File.join(File.dirname(__FILE__), "harvest", "harvest_resource")
|
25
|
+
|
26
|
+
# Shortcut for Harvest::Base.new
|
27
|
+
#
|
28
|
+
# Example:
|
29
|
+
# Harvest(:email => "jack@exampe.com",
|
30
|
+
# :password => "secret",
|
31
|
+
# :sub_domain => "frenchie",
|
32
|
+
# :headers => {"User-Agent => "Harvest Rubygem"})
|
33
|
+
def Harvest(options={})
|
34
|
+
Harvest::Base.new(options)
|
35
|
+
end
|
data/lib/harvest/base.rb
ADDED
@@ -0,0 +1,77 @@
|
|
1
|
+
module Harvest
|
2
|
+
class Base
|
3
|
+
|
4
|
+
# Requires a sub_domain, email, and password.
|
5
|
+
# Specifying headers is optional, but useful for setting a user agent.
|
6
|
+
def initialize(options={})
|
7
|
+
options.assert_valid_keys(:email, :password, :sub_domain, :headers, :ssl)
|
8
|
+
options.assert_required_keys(:email, :password, :sub_domain)
|
9
|
+
@email = options[:email]
|
10
|
+
@password = options[:password]
|
11
|
+
@sub_domain = options[:sub_domain]
|
12
|
+
@headers = options[:headers]
|
13
|
+
@ssl = options[:ssl]
|
14
|
+
configure_base_resource
|
15
|
+
end
|
16
|
+
|
17
|
+
# Below is a series of proxies allowing for easy
|
18
|
+
# access to the various resources.
|
19
|
+
|
20
|
+
# Clients
|
21
|
+
def clients
|
22
|
+
Harvest::Resources::Client
|
23
|
+
end
|
24
|
+
|
25
|
+
# Expenses.
|
26
|
+
def expenses
|
27
|
+
Harvest::Resources::Expense
|
28
|
+
end
|
29
|
+
|
30
|
+
# Expense categories.
|
31
|
+
def expense_categories
|
32
|
+
Harvest::Resources::ExpenseCategory
|
33
|
+
end
|
34
|
+
|
35
|
+
# People.
|
36
|
+
# Also provides access to time entries.
|
37
|
+
def people
|
38
|
+
Harvest::Resources::Person
|
39
|
+
end
|
40
|
+
|
41
|
+
# Projects.
|
42
|
+
# Provides access to the assigned users and tasks
|
43
|
+
# along with reports for entries on the project.
|
44
|
+
def projects
|
45
|
+
Harvest::Resources::Project
|
46
|
+
end
|
47
|
+
|
48
|
+
# Tasks.
|
49
|
+
def tasks
|
50
|
+
Harvest::Resources::Task
|
51
|
+
end
|
52
|
+
|
53
|
+
# Invoices
|
54
|
+
def invoices
|
55
|
+
Harvest::Resources::Invoice
|
56
|
+
end
|
57
|
+
|
58
|
+
private
|
59
|
+
|
60
|
+
# Configure resource base class so that
|
61
|
+
# inherited classes can access the api.
|
62
|
+
def configure_base_resource
|
63
|
+
HarvestResource.site = "http#{'s' if @ssl}://#{@sub_domain}.#{Harvest::ApiDomain}"
|
64
|
+
HarvestResource.user = @email
|
65
|
+
HarvestResource.password = @password
|
66
|
+
HarvestResource.headers.update(@headers) if @headers.is_a?(Hash)
|
67
|
+
load_resources
|
68
|
+
end
|
69
|
+
|
70
|
+
# Load the classes representing the various resources.
|
71
|
+
def load_resources
|
72
|
+
resource_path = File.join(File.dirname(__FILE__), "resources")
|
73
|
+
Harvest.load_all_ruby_files_from_path(resource_path)
|
74
|
+
end
|
75
|
+
|
76
|
+
end
|
77
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module Harvest
|
2
|
+
# This is the base class from which all resource
|
3
|
+
# classes inherit. Site and authentication params
|
4
|
+
# are loaded into this class when a Harvest::Base
|
5
|
+
# object is initialized.
|
6
|
+
class HarvestResource < ActiveResource::Base
|
7
|
+
include ActiveResourceThrottle
|
8
|
+
include Harvest::Plugins::ActiveResourceInheritableHeaders
|
9
|
+
|
10
|
+
# The harvest api will block requests in excess
|
11
|
+
# of 40 / 15 seconds. Adds a throttle (with cautious settings).
|
12
|
+
# Throttle feature provided by activeresource_throttle gem.
|
13
|
+
self.throttle(:requests => 30, :interval => 15, :sleep_interval => 60)
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
# Allows headers in an ActiveResource::Base class
|
2
|
+
# to be inherited by subclasses. Useful for setting
|
3
|
+
# a User-Agent used by all resources.
|
4
|
+
module Harvest
|
5
|
+
module Plugins
|
6
|
+
module ActiveResourceInheritableHeaders
|
7
|
+
|
8
|
+
module ClassMethods
|
9
|
+
|
10
|
+
# If headers are not defined in a
|
11
|
+
# given subclass, then obtain headers
|
12
|
+
# from the superclass.
|
13
|
+
def inheritable_headers
|
14
|
+
if defined?(@headers)
|
15
|
+
@headers
|
16
|
+
elsif superclass != Object && superclass.headers
|
17
|
+
superclass.headers
|
18
|
+
else
|
19
|
+
@headers ||= {}
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.included(klass)
|
26
|
+
klass.instance_eval do
|
27
|
+
klass.extend ClassMethods
|
28
|
+
class << self
|
29
|
+
alias_method :headers, :inheritable_headers
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# The entry resource is never accessed directly;
|
2
|
+
# rather, it is manipulated through an instance
|
3
|
+
# of Project or Person.
|
4
|
+
module Harvest
|
5
|
+
module Resources
|
6
|
+
class Entry < Harvest::HarvestResource
|
7
|
+
|
8
|
+
self.element_name = "entry"
|
9
|
+
|
10
|
+
class << self
|
11
|
+
|
12
|
+
def project_id=(id)
|
13
|
+
@project_id = id
|
14
|
+
self.site = self.site + "/projects/#{@project_id}"
|
15
|
+
end
|
16
|
+
|
17
|
+
def project_id
|
18
|
+
@project_id
|
19
|
+
end
|
20
|
+
|
21
|
+
def person_id=(id)
|
22
|
+
@person_id = id
|
23
|
+
self.site = self.site + "/people/#{@person_id}"
|
24
|
+
end
|
25
|
+
|
26
|
+
def person_id
|
27
|
+
@person_id
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|
31
|
+
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|