harvested2 5.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.document +3 -0
- data/.gitignore +35 -0
- data/.rspec +1 -0
- data/.rubocop.yml +34 -0
- data/.ruby-version +1 -0
- data/.travis.yml +12 -0
- data/Gemfile +20 -0
- data/HISTORY.md +118 -0
- data/MIT-LICENSE +21 -0
- data/README.md +66 -0
- data/Rakefile +24 -0
- data/harvested2.gemspec +30 -0
- data/lib/ext/array.rb +52 -0
- data/lib/ext/date.rb +9 -0
- data/lib/ext/hash.rb +17 -0
- data/lib/ext/time.rb +5 -0
- data/lib/harvest/account.rb +13 -0
- data/lib/harvest/api/account.rb +25 -0
- data/lib/harvest/api/base.rb +72 -0
- data/lib/harvest/api/clients.rb +10 -0
- data/lib/harvest/api/company.rb +12 -0
- data/lib/harvest/api/contacts.rb +9 -0
- data/lib/harvest/api/expense_categories.rb +9 -0
- data/lib/harvest/api/expenses.rb +26 -0
- data/lib/harvest/api/invoice_categories.rb +9 -0
- data/lib/harvest/api/invoice_messages.rb +86 -0
- data/lib/harvest/api/invoice_payments.rb +41 -0
- data/lib/harvest/api/invoices.rb +9 -0
- data/lib/harvest/api/projects.rb +9 -0
- data/lib/harvest/api/task_assignments.rb +75 -0
- data/lib/harvest/api/tasks.rb +9 -0
- data/lib/harvest/api/time_entry.rb +19 -0
- data/lib/harvest/api/user_assignments.rb +75 -0
- data/lib/harvest/api/users.rb +10 -0
- data/lib/harvest/base.rb +333 -0
- data/lib/harvest/behavior/activatable.rb +31 -0
- data/lib/harvest/behavior/crud.rb +80 -0
- data/lib/harvest/client.rb +23 -0
- data/lib/harvest/company.rb +8 -0
- data/lib/harvest/contact.rb +20 -0
- data/lib/harvest/credentials.rb +34 -0
- data/lib/harvest/errors.rb +27 -0
- data/lib/harvest/expense.rb +54 -0
- data/lib/harvest/expense_category.rb +10 -0
- data/lib/harvest/hardy_client.rb +80 -0
- data/lib/harvest/invoice.rb +75 -0
- data/lib/harvest/invoice_category.rb +8 -0
- data/lib/harvest/invoice_message.rb +8 -0
- data/lib/harvest/invoice_payment.rb +8 -0
- data/lib/harvest/line_item.rb +21 -0
- data/lib/harvest/model.rb +133 -0
- data/lib/harvest/project.rb +41 -0
- data/lib/harvest/receipt.rb +12 -0
- data/lib/harvest/task.rb +21 -0
- data/lib/harvest/task_assignment.rb +27 -0
- data/lib/harvest/time_entry.rb +57 -0
- data/lib/harvest/timezones.rb +130 -0
- data/lib/harvest/user.rb +58 -0
- data/lib/harvest/user_assignment.rb +27 -0
- data/lib/harvest/version.rb +3 -0
- data/lib/harvested2.rb +96 -0
- data/spec/factories/client.rb +14 -0
- data/spec/factories/contact.rb +8 -0
- data/spec/factories/expense.rb +10 -0
- data/spec/factories/expenses_category.rb +7 -0
- data/spec/factories/invoice.rb +25 -0
- data/spec/factories/invoice_category.rb +5 -0
- data/spec/factories/invoice_message.rb +9 -0
- data/spec/factories/invoice_payment.rb +7 -0
- data/spec/factories/line_item.rb +9 -0
- data/spec/factories/project.rb +15 -0
- data/spec/factories/task.rb +8 -0
- data/spec/factories/task_assignment.rb +8 -0
- data/spec/factories/time_entry.rb +13 -0
- data/spec/factories/user.rb +19 -0
- data/spec/factories/user_assigment.rb +7 -0
- data/spec/functional/clients_spec.rb +105 -0
- data/spec/functional/errors_spec.rb +42 -0
- data/spec/functional/expenses_spec.rb +97 -0
- data/spec/functional/invoice_messages_spec.rb +48 -0
- data/spec/functional/invoice_payments_spec.rb +51 -0
- data/spec/functional/invoice_spec.rb +138 -0
- data/spec/functional/project_spec.rb +76 -0
- data/spec/functional/tasks_spec.rb +119 -0
- data/spec/functional/time_entries_spec.rb +87 -0
- data/spec/functional/users_spec.rb +72 -0
- data/spec/harvest/base_spec.rb +10 -0
- data/spec/harvest/basic_auth_credentials_spec.rb +12 -0
- data/spec/harvest/expense_category_spec.rb +5 -0
- data/spec/harvest/expense_spec.rb +18 -0
- data/spec/harvest/invoice_message_spec.rb +5 -0
- data/spec/harvest/invoice_payment_spec.rb +5 -0
- data/spec/harvest/invoice_spec.rb +5 -0
- data/spec/harvest/oauth_credentials_spec.rb +11 -0
- data/spec/harvest/project_spec.rb +5 -0
- data/spec/harvest/task_assignment_spec.rb +5 -0
- data/spec/harvest/task_spec.rb +5 -0
- data/spec/harvest/time_entry_spec.rb +23 -0
- data/spec/harvest/user_assignment_spec.rb +5 -0
- data/spec/harvest/user_spec.rb +34 -0
- data/spec/spec_helper.rb +22 -0
- data/spec/support/factory_bot.rb +5 -0
- data/spec/support/harvested_helpers.rb +28 -0
- data/spec/support/json_examples.rb +9 -0
- metadata +238 -0
data/lib/harvest/user.rb
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
module Harvest
|
|
2
|
+
# The model that contains information about a task
|
|
3
|
+
#
|
|
4
|
+
# == Fields
|
|
5
|
+
# [+id+] (READONLY) the id of the user
|
|
6
|
+
# [+email+] the email of the user
|
|
7
|
+
# [+first_name+] the first name for the user
|
|
8
|
+
# [+last_name+] the last name for the user
|
|
9
|
+
# [+telephone+] the telephone for the user
|
|
10
|
+
# [+department] the department for the user
|
|
11
|
+
# [+has_access_to_all_future_projects+] whether the user should be added to future projects by default
|
|
12
|
+
# [+hourly_rate+] what the default hourly rate for the user is
|
|
13
|
+
# [+admin?+] whether the user is an admin
|
|
14
|
+
# [+contractor?+] whether the user is a contractor
|
|
15
|
+
# [+contractor?+] whether the user is a contractor
|
|
16
|
+
# [+timezone+] the timezone for the user.
|
|
17
|
+
class User < Hashie::Mash
|
|
18
|
+
include Harvest::Model
|
|
19
|
+
|
|
20
|
+
skip_json_root true
|
|
21
|
+
api_path '/users'
|
|
22
|
+
|
|
23
|
+
delegate_methods(active?: :is_active,
|
|
24
|
+
admin?: :is_admin,
|
|
25
|
+
contractor?: :is_contractor)
|
|
26
|
+
|
|
27
|
+
def initialize(args = {}, _ = nil)
|
|
28
|
+
args = args.stringify_keys
|
|
29
|
+
args['is_admin'] = args.delete('admin') if args['admin']
|
|
30
|
+
self.timezone = args.delete('timezone') if args['timezone']
|
|
31
|
+
super
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Sets the timezone for the user. This can be done in a variety of ways.
|
|
35
|
+
#
|
|
36
|
+
# == Examples
|
|
37
|
+
# user.timezone = :cst # the easiest way. CST, EST, MST, and PST are supported
|
|
38
|
+
#
|
|
39
|
+
# user.timezone = 'america/chicago' # a little more verbose
|
|
40
|
+
#
|
|
41
|
+
# user.timezone = 'Central Time (US & Canada)' # the most explicit way
|
|
42
|
+
def timezone=(timezone)
|
|
43
|
+
tz = timezone.to_s.downcase
|
|
44
|
+
case tz
|
|
45
|
+
when 'cst', 'cdt' then self.timezone = 'america/chicago'
|
|
46
|
+
when 'est', 'edt' then self.timezone = 'america/new_york'
|
|
47
|
+
when 'mst', 'mdt' then self.timezone = 'america/denver'
|
|
48
|
+
when 'pst', 'pdt' then self.timezone = 'america/los_angeles'
|
|
49
|
+
else
|
|
50
|
+
if Harvest::Timezones::MAPPING[tz]
|
|
51
|
+
self['timezone'] = Harvest::Timezones::MAPPING[tz]
|
|
52
|
+
else
|
|
53
|
+
self['timezone'] = timezone
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
module Harvest
|
|
2
|
+
class UserAssignment < Hashie::Mash
|
|
3
|
+
include Harvest::Model
|
|
4
|
+
|
|
5
|
+
skip_json_root true
|
|
6
|
+
|
|
7
|
+
def initialize(args = {}, _ = nil)
|
|
8
|
+
args = args.stringify_keys
|
|
9
|
+
self.user = args.delete('user') if args['user']
|
|
10
|
+
self.project = args.delete('project') if args['project']
|
|
11
|
+
|
|
12
|
+
super
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def user=(user)
|
|
16
|
+
self['user_id'] = user['id']
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def project=(project)
|
|
20
|
+
self['project_id'] = project['id']
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def active?
|
|
24
|
+
!deactivated
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
data/lib/harvested2.rb
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
require 'httparty'
|
|
2
|
+
require 'base64'
|
|
3
|
+
require 'delegate'
|
|
4
|
+
require 'hashie'
|
|
5
|
+
require 'json'
|
|
6
|
+
require 'time'
|
|
7
|
+
require 'csv'
|
|
8
|
+
|
|
9
|
+
require 'ext/array'
|
|
10
|
+
require 'ext/hash'
|
|
11
|
+
require 'ext/date'
|
|
12
|
+
require 'ext/time'
|
|
13
|
+
|
|
14
|
+
require 'harvest/version'
|
|
15
|
+
require 'harvest/credentials'
|
|
16
|
+
require 'harvest/errors'
|
|
17
|
+
require 'harvest/hardy_client'
|
|
18
|
+
require 'harvest/timezones'
|
|
19
|
+
|
|
20
|
+
require 'harvest/base'
|
|
21
|
+
|
|
22
|
+
%w(crud activatable).each { |a| require "harvest/behavior/#{a}" }
|
|
23
|
+
|
|
24
|
+
%w(model client contact project task user task_assignment
|
|
25
|
+
user_assignment expense_category expense time_entry invoice_category
|
|
26
|
+
line_item invoice invoice_payment
|
|
27
|
+
invoice_message).each { |a| require "harvest/#{a}" }
|
|
28
|
+
|
|
29
|
+
%w(base account clients contacts projects tasks users task_assignments
|
|
30
|
+
user_assignments expense_categories expenses time_entry
|
|
31
|
+
invoice_categories invoices invoice_payments
|
|
32
|
+
invoice_messages).each { |a| require "harvest/api/#{a}" }
|
|
33
|
+
|
|
34
|
+
module Harvest
|
|
35
|
+
class << self
|
|
36
|
+
|
|
37
|
+
# Creates a standard client that will raise all errors it encounters
|
|
38
|
+
#
|
|
39
|
+
# == Options
|
|
40
|
+
# * Basic Authentication V2
|
|
41
|
+
# * +:access_token+ - Your Harvest access_token
|
|
42
|
+
# * +:account_id+ - Your Harvest account_id
|
|
43
|
+
# * +:client_id+ - An OAuth 2.0 access token
|
|
44
|
+
#
|
|
45
|
+
# == Examples
|
|
46
|
+
# Harvest.client(access_token: 'token', account_id: '123')
|
|
47
|
+
# Harvest.client(access_token: 'myaccesstoken', client_id: '1')
|
|
48
|
+
#
|
|
49
|
+
# @return [Harvest::Base]
|
|
50
|
+
def client(access_token: nil, account_id: nil, client_id: nil)
|
|
51
|
+
Harvest::Base.new(access_token: access_token, account_id: account_id,
|
|
52
|
+
client_id: client_id)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Creates a hardy client that will retry common HTTP errors
|
|
56
|
+
# it encounters and sleep() if it determines it is over your rate limit
|
|
57
|
+
#
|
|
58
|
+
# == Options
|
|
59
|
+
# * Basic Authentication
|
|
60
|
+
# * +:access_token+ - Your Harvest access_token
|
|
61
|
+
# * +:account_id+ - Your Harvest account_id
|
|
62
|
+
# * +:client_id+ - An OAuth 2.0 access token
|
|
63
|
+
# * +:retry+ - How many times the hardy client should retry errors.
|
|
64
|
+
# Set to +5+ by default.
|
|
65
|
+
#
|
|
66
|
+
# == Examples
|
|
67
|
+
# Harvest.hardy_client(access_token: 'token', account_id: '123', retry: 3)
|
|
68
|
+
#
|
|
69
|
+
# Harvest.hardy_client(access_token: 'myaccesstoken', client_id: '1',
|
|
70
|
+
# retries: 3)
|
|
71
|
+
#
|
|
72
|
+
# == Errors
|
|
73
|
+
# The hardy client will retry the following errors
|
|
74
|
+
# * Harvest::Unavailable
|
|
75
|
+
# * Harvest::InformHarvest
|
|
76
|
+
# * Net::HTTPError
|
|
77
|
+
# * Net::HTTPFatalError
|
|
78
|
+
# * Errno::ECONNRESET
|
|
79
|
+
#
|
|
80
|
+
# == Rate Limits
|
|
81
|
+
# The hardy client will make as many requests as it can until it detects it
|
|
82
|
+
# has gone over the rate limit. Then it will +sleep()+ for the how ever
|
|
83
|
+
# long it takes for the limit to reset. You can find more information about
|
|
84
|
+
# the Rate Limiting at http://www.getharvest.com/api
|
|
85
|
+
#
|
|
86
|
+
# @return [Harvest::HardyClient] a Harvest::Base wrapped in a Harvest::HardyClient
|
|
87
|
+
# @see Harvest::Base
|
|
88
|
+
def hardy_client(access_token: nil, account_id: nil, client_id: nil, retries: 5)
|
|
89
|
+
Harvest::HardyClient.new(client(
|
|
90
|
+
access_token: access_token,
|
|
91
|
+
account_id: account_id,
|
|
92
|
+
client_id: client_id),
|
|
93
|
+
retries)
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
FactoryBot.define do
|
|
2
|
+
factory :invoice, class: Harvest::Invoice do
|
|
3
|
+
sequence(:id)
|
|
4
|
+
subject "Invoice for Joe's Stream Cleaning"
|
|
5
|
+
amount 2400.0
|
|
6
|
+
issued_at '2014-01-01'
|
|
7
|
+
due_at "2011-03-31"
|
|
8
|
+
due_at_human_format 'upon receipt'
|
|
9
|
+
currency 'United States Dollars - USD'
|
|
10
|
+
number '1000'
|
|
11
|
+
notes 'Some notes go here'
|
|
12
|
+
period_end '2013-03-31'
|
|
13
|
+
period_start '2013-02-26'
|
|
14
|
+
kind 'free_form'
|
|
15
|
+
state 'draft'
|
|
16
|
+
purchase_order nil
|
|
17
|
+
tax nil
|
|
18
|
+
tax2 nil
|
|
19
|
+
import_hours 'no'
|
|
20
|
+
import_expenses 'no'
|
|
21
|
+
|
|
22
|
+
client
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
FactoryBot.define do
|
|
2
|
+
factory :user, class: Harvest::User do
|
|
3
|
+
sequence(:id)
|
|
4
|
+
first_name 'Edgar'
|
|
5
|
+
last_name 'Ruth'
|
|
6
|
+
email 'edgar@ruth.com'
|
|
7
|
+
timezone 'cst'
|
|
8
|
+
is_admin 'false'
|
|
9
|
+
telephone '444-4444'
|
|
10
|
+
|
|
11
|
+
trait :active do
|
|
12
|
+
is_active true
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
trait :deactive do
|
|
16
|
+
is_active false
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
|
|
3
|
+
describe 'harvest clients' do
|
|
4
|
+
let(:harvest) { Harvest.client(access_token: 'mytoken', account_id: '123') }
|
|
5
|
+
|
|
6
|
+
describe 'clients' do
|
|
7
|
+
let(:client) { create(:client) }
|
|
8
|
+
let(:client_attributes) { FactoryBot.attributes_for(:client) }
|
|
9
|
+
|
|
10
|
+
context 'allows to add clients' do
|
|
11
|
+
before do
|
|
12
|
+
allow(harvest.clients).to receive(:create).and_return(client)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
it 'returns true' do
|
|
16
|
+
expect(client.name).to eql(client_attributes[:name])
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
context 'allows to update clients' do
|
|
21
|
+
before do
|
|
22
|
+
allow(harvest.clients).to receive(:update).and_return(client)
|
|
23
|
+
client.name = "Joe and Frank's Steam Cleaning"
|
|
24
|
+
client = harvest.clients.update(client)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
it 'returns true' do
|
|
28
|
+
expect(client.name).to eql("Joe and Frank's Steam Cleaning")
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
context 'allows to remove clients' do
|
|
33
|
+
before do
|
|
34
|
+
allow(harvest.clients).to receive(:delete).and_return([])
|
|
35
|
+
allow(harvest.clients).to receive(:all).and_return([])
|
|
36
|
+
harvest.clients.delete(client)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
it 'returns true' do
|
|
40
|
+
expect(harvest.clients.all.select do |p|
|
|
41
|
+
p.name == "Joe and Frank's Steam Cleaning"
|
|
42
|
+
end).to eql([])
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
context 'allows activating and deactivating clients' do
|
|
47
|
+
let(:active_client) { create(:client, :active) }
|
|
48
|
+
let(:deactive_client) { create(:client, :deactive) }
|
|
49
|
+
|
|
50
|
+
before do
|
|
51
|
+
allow(harvest.clients).to receive(:deactivate)
|
|
52
|
+
.and_return(deactive_client)
|
|
53
|
+
allow(harvest.clients).to receive(:activate)
|
|
54
|
+
.and_return(active_client)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
it 'should be active' do
|
|
58
|
+
expect(active_client).to be_active
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
it 'should be deactive' do
|
|
62
|
+
client = harvest.clients.deactivate(client)
|
|
63
|
+
expect(deactive_client).not_to be_active
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
it 'should reactive a client' do
|
|
67
|
+
client = harvest.clients.activate(client)
|
|
68
|
+
expect(active_client).to be_active
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
describe 'contacts' do
|
|
74
|
+
let(:client) { create(:client) }
|
|
75
|
+
let(:contact) { create(:contact, client: client) }
|
|
76
|
+
let(:contact_attributes) { FactoryBot.attributes_for(:contact) }
|
|
77
|
+
|
|
78
|
+
context 'allows to add contacts' do
|
|
79
|
+
before do
|
|
80
|
+
allow(harvest.contacts).to receive(:create)
|
|
81
|
+
.and_return(contact)
|
|
82
|
+
contact = harvest.contacts.create(contact_attributes)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
it 'should be true' do
|
|
86
|
+
expect(contact.client_id).to eql(client.id)
|
|
87
|
+
expect(contact.email).to eql('jane@example.com')
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
context 'allows to delete contacts' do
|
|
92
|
+
before do
|
|
93
|
+
allow(harvest.contacts).to receive(:delete).and_return([])
|
|
94
|
+
allow(harvest.contacts).to receive(:all).and_return([])
|
|
95
|
+
contact = harvest.contacts.delete(contact)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
it 'should return empty' do
|
|
99
|
+
expect(harvest.contacts.all.select do |e|
|
|
100
|
+
e.email == 'jane@example.com'
|
|
101
|
+
end).to eql([])
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|