harv 0.8.3
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/base.rb +118 -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 +34 -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/lib/harvest.rb +37 -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 +84 -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 +137 -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/base.rb
ADDED
@@ -0,0 +1,118 @@
|
|
1
|
+
module Harvest
|
2
|
+
class Base
|
3
|
+
@debug_level = 0
|
4
|
+
|
5
|
+
def self.debug_level
|
6
|
+
@debug_level
|
7
|
+
end
|
8
|
+
|
9
|
+
def self.debug_level=(debug_level)
|
10
|
+
raise ArgumentError, "debug level must be an integer" unless debug_level == debug_level.to_i
|
11
|
+
|
12
|
+
return @debug_level if @debug_level == debug_level
|
13
|
+
|
14
|
+
@debug_level = debug_level.to_i
|
15
|
+
|
16
|
+
ActiveSupport::Notifications.unsubscribe(@subscriber) if @subscriber
|
17
|
+
|
18
|
+
case @debug_level
|
19
|
+
when 0 then
|
20
|
+
when 1 then
|
21
|
+
@subscriber = ActiveSupport::Notifications.subscribe(/request.active_resource/) do |*args|
|
22
|
+
event = ActiveSupport::Notifications::Event.new(*args)
|
23
|
+
puts "-- HARVEST #{event.payload[:method].to_s.upcase} #{event.payload[:request_uri]}, #{event.payload[:result].andand.code}: #{event.payload[:result].andand.message}"
|
24
|
+
end
|
25
|
+
else
|
26
|
+
@subscriber = ActiveSupport::Notifications.subscribe(/request.active_resource/) do |*args|
|
27
|
+
event = ActiveSupport::Notifications::Event.new(*args)
|
28
|
+
puts "-- HARVEST #{event.payload[:method].to_s.upcase} #{event.payload[:request_uri]}, #{event.payload[:result].andand.code}: #{event.payload[:result].andand.message}"
|
29
|
+
puts event.payload[:result].body + "\n"
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
@debug_level
|
34
|
+
end
|
35
|
+
|
36
|
+
# Requires a sub_domain, email, and password.
|
37
|
+
# Specifying headers is optional, but useful for setting a user agent.
|
38
|
+
def initialize(options={})
|
39
|
+
options.assert_required_keys(:email, :password, :sub_domain)
|
40
|
+
@email = options[:email]
|
41
|
+
@password = options[:password]
|
42
|
+
@sub_domain = options[:sub_domain]
|
43
|
+
@headers = options[:headers]
|
44
|
+
@ssl = options[:ssl]
|
45
|
+
configure_base_resource
|
46
|
+
end
|
47
|
+
|
48
|
+
# Below is a series of proxies allowing for easy
|
49
|
+
# access to the various resources.
|
50
|
+
|
51
|
+
# Clients
|
52
|
+
def clients
|
53
|
+
Harvest::Resources::Client
|
54
|
+
end
|
55
|
+
|
56
|
+
# Contacts
|
57
|
+
def contacts
|
58
|
+
Harvest::Resources::Contact
|
59
|
+
end
|
60
|
+
|
61
|
+
# Expenses.
|
62
|
+
def expenses
|
63
|
+
Harvest::Resources::Expense
|
64
|
+
end
|
65
|
+
|
66
|
+
# Expense categories.
|
67
|
+
def expense_categories
|
68
|
+
Harvest::Resources::ExpenseCategory
|
69
|
+
end
|
70
|
+
|
71
|
+
# People.
|
72
|
+
# Also provides access to time entries.
|
73
|
+
def people
|
74
|
+
Harvest::Resources::Person
|
75
|
+
end
|
76
|
+
|
77
|
+
# Projects.
|
78
|
+
# Provides access to the assigned users and tasks
|
79
|
+
# along with reports for entries on the project.
|
80
|
+
def projects
|
81
|
+
Harvest::Resources::Project
|
82
|
+
end
|
83
|
+
|
84
|
+
# Tasks.
|
85
|
+
def tasks
|
86
|
+
Harvest::Resources::Task
|
87
|
+
end
|
88
|
+
|
89
|
+
# Invoices
|
90
|
+
def invoices
|
91
|
+
Harvest::Resources::Invoice
|
92
|
+
end
|
93
|
+
|
94
|
+
# Invoice Messages
|
95
|
+
def invoice_messages
|
96
|
+
Harvest::Resources::InvoiceMessage
|
97
|
+
end
|
98
|
+
|
99
|
+
private
|
100
|
+
|
101
|
+
# Configure resource base class so that
|
102
|
+
# inherited classes can access the api.
|
103
|
+
def configure_base_resource
|
104
|
+
HarvestResource.site = "http#{'s' if @ssl}://#{@sub_domain}.#{Harvest::ApiDomain}"
|
105
|
+
HarvestResource.user = @email
|
106
|
+
HarvestResource.password = @password
|
107
|
+
HarvestResource.headers.update(@headers) if @headers.is_a?(Hash)
|
108
|
+
load_resources
|
109
|
+
end
|
110
|
+
|
111
|
+
# Load the classes representing the various resources.
|
112
|
+
def load_resources
|
113
|
+
resource_path = File.join(File.dirname(__FILE__), "resources")
|
114
|
+
Harvest.load_all_ruby_files_from_path(resource_path)
|
115
|
+
end
|
116
|
+
|
117
|
+
end
|
118
|
+
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
|
+
module Harvest
|
2
|
+
module Resources
|
3
|
+
class Client < Harvest::HarvestResource
|
4
|
+
include Harvest::Plugins::Toggleable
|
5
|
+
|
6
|
+
def invoices(refresh = false)
|
7
|
+
if not refresh and @invoices
|
8
|
+
@invoices
|
9
|
+
else
|
10
|
+
page, @invoices = 1, []
|
11
|
+
begin
|
12
|
+
set = Invoice.find(:all, :params => {:client => self.id, :page => page })
|
13
|
+
puts "Found #{set.length.to_s} invoices" if Harvest::Base.debug_level == 2
|
14
|
+
@invoices += set
|
15
|
+
page +=1
|
16
|
+
end while set.length == 50
|
17
|
+
@invoices
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def balance
|
22
|
+
invoices.select {|invoice| invoice.sent? }.inject(0) {|total, invoice| total + invoice.balance}
|
23
|
+
end
|
24
|
+
|
25
|
+
def invoiced_amount
|
26
|
+
invoices.select {|invoice| invoice.sent? }.inject(0) {|total, invoice| total + invoice.amount}
|
27
|
+
end
|
28
|
+
|
29
|
+
def paid_amount
|
30
|
+
invoices.select {|invoice| invoice.sent? }.inject(0) {|total, invoice| total + invoice.paid_amount}
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
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
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module Harvest
|
2
|
+
module Resources
|
3
|
+
class Expense < Harvest::HarvestResource
|
4
|
+
|
5
|
+
self.element_name = "expense"
|
6
|
+
|
7
|
+
class << self
|
8
|
+
|
9
|
+
def person_id=(id)
|
10
|
+
@person_id = id
|
11
|
+
self.site = self.site + "/people/#{@person_id}"
|
12
|
+
end
|
13
|
+
|
14
|
+
def person_id
|
15
|
+
@person_id
|
16
|
+
end
|
17
|
+
|
18
|
+
end
|
19
|
+
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
module Harvest
|
2
|
+
module Resources
|
3
|
+
class Person < Harvest::HarvestResource
|
4
|
+
include Harvest::Plugins::Toggleable
|
5
|
+
|
6
|
+
# Find all entries for the given person;
|
7
|
+
# options[:from] and options[:to] are required;
|
8
|
+
# include options[:user_id] to limit by a specific project.
|
9
|
+
def entries(options={})
|
10
|
+
validate_options(options)
|
11
|
+
entry_class = Harvest::Resources::Entry.clone
|
12
|
+
entry_class.person_id = self.id
|
13
|
+
entry_class.find :all, :params => format_params(options)
|
14
|
+
end
|
15
|
+
|
16
|
+
def expenses(options={})
|
17
|
+
validate_options(options)
|
18
|
+
expense_class = Harvest::Resources::Expense.clone
|
19
|
+
expense_class.person_id = self.id
|
20
|
+
expense_class.find :all, :params => format_params(options)
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def validate_options(options)
|
26
|
+
if [:from, :to].any? {|key| !options[key].respond_to?(:strftime) }
|
27
|
+
raise ArgumentError, "Must specify :from and :to as dates."
|
28
|
+
end
|
29
|
+
|
30
|
+
if options[:from] > options[:to]
|
31
|
+
raise ArgumentError, ":start must precede :end."
|
32
|
+
end
|
33
|
+
|
34
|
+
end
|
35
|
+
def format_params(options)
|
36
|
+
ops = { :from => options[:from].strftime("%Y%m%d"),
|
37
|
+
:to => options[:to].strftime("%Y%m%d")}
|
38
|
+
ops[:project_id] = options[:project_id] if options[:project_id]
|
39
|
+
return ops
|
40
|
+
end
|
41
|
+
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
module Harvest
|
2
|
+
module Resources
|
3
|
+
# Supports the following:
|
4
|
+
class Project < Harvest::HarvestResource
|
5
|
+
include Harvest::Plugins::Toggleable
|
6
|
+
|
7
|
+
def users
|
8
|
+
user_class = Harvest::Resources::UserAssignment.clone
|
9
|
+
user_class.project_id = self.id
|
10
|
+
user_class
|
11
|
+
end
|
12
|
+
|
13
|
+
def tasks
|
14
|
+
task_class = Harvest::Resources::TaskAssignment.clone
|
15
|
+
task_class.project_id = self.id
|
16
|
+
task_class
|
17
|
+
end
|
18
|
+
|
19
|
+
# Find all entries for the given project;
|
20
|
+
# options[:from] and options[:to] are required;
|
21
|
+
# include options[:user_id] to limit by a specific user.
|
22
|
+
#
|
23
|
+
def entries(options={})
|
24
|
+
validate_entries_options(options)
|
25
|
+
entry_class = Harvest::Resources::Entry.clone
|
26
|
+
entry_class.project_id = self.id
|
27
|
+
entry_class.find :all, :params => format_params(options)
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
def validate_entries_options(options)
|
33
|
+
if [:from, :to].any? {|key| !options[key].respond_to?(:strftime) }
|
34
|
+
raise ArgumentError, "Must specify :from and :to as dates."
|
35
|
+
end
|
36
|
+
|
37
|
+
if options[:from] > options[:to]
|
38
|
+
raise ArgumentError, ":start must precede :end."
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def format_params(options)
|
43
|
+
ops = { :from => options[:from].strftime("%Y%m%d"),
|
44
|
+
:to => options[:to].strftime("%Y%m%d")}
|
45
|
+
ops[:user_id] = options[:user_id] if options[:user_id]
|
46
|
+
return ops
|
47
|
+
end
|
48
|
+
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# This class is accessed by an instance of Project.
|
2
|
+
module Harvest
|
3
|
+
module Resources
|
4
|
+
class TaskAssignment < Harvest::HarvestResource
|
5
|
+
|
6
|
+
self.element_name = "task_assignment"
|
7
|
+
|
8
|
+
class << self
|
9
|
+
|
10
|
+
def project_id=(id)
|
11
|
+
@project_id = id
|
12
|
+
set_site
|
13
|
+
end
|
14
|
+
|
15
|
+
def project_id
|
16
|
+
@project_id
|
17
|
+
end
|
18
|
+
|
19
|
+
def set_site
|
20
|
+
self.site = self.site + "/projects/#{self.project_id}"
|
21
|
+
end
|
22
|
+
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# This class is accessed by an instance of Project.
|
2
|
+
module Harvest
|
3
|
+
module Resources
|
4
|
+
class UserAssignment < Harvest::HarvestResource
|
5
|
+
|
6
|
+
self.element_name = "user_assignment"
|
7
|
+
|
8
|
+
class << self
|
9
|
+
|
10
|
+
def project_id=(id)
|
11
|
+
@project_id = id
|
12
|
+
set_site
|
13
|
+
end
|
14
|
+
|
15
|
+
def project_id
|
16
|
+
@project_id
|
17
|
+
end
|
18
|
+
|
19
|
+
def set_site
|
20
|
+
self.site = self.site + "/projects/#{self.project_id}"
|
21
|
+
end
|
22
|
+
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|