harvesting 0.3.0 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 420911f1f064b300fb9db0e3490a23cee246fd1e8f885ac631558818a9afa761
4
- data.tar.gz: cbf6caa05a4092e344e1638dab33081478a973282d3d1a9f2ed3cb30ec75f7ab
3
+ metadata.gz: 4452c68eed7a6097355f504450d77cf13e72424ca0ec6915fb041dd8abb155f7
4
+ data.tar.gz: 82e16401985e4e19cd14ac1fb57f13b9f87f2d29188800e584516a68c3412ca7
5
5
  SHA512:
6
- metadata.gz: 3b9a22eb96ff64f1f0947b22d026311635419b29616e56a01baca381fac7ff0d61cbe1ff29597d24d461fb1a58d92db4edf755ac7c5ac206b71002e5bc3442f7
7
- data.tar.gz: 398e22c481a9f56e543beebb87a0b0572e725e8f2dd5eb61ea78176bec5f56d6afac652e1a9477754eb608fc612fe45d212fed8fb2034e1724997cce017855a1
6
+ metadata.gz: cf1b81f56a3ec9c5efc9c19816ea6da487927fc00f49d255e58f647d918de5e9ce5a54b5e6bf1c848fb0002e3239f1d25590720acaf0c84bbbc12247ef634b84
7
+ data.tar.gz: a40d1ec6516ef528129aedc4747943189c19b3da0b5cf4ac950be1e735c06814008a192a303d2cf9a2f93b0336d0fae988be72fcaa8f5d1953511453a08b1a0d
@@ -1,7 +1,10 @@
1
- HARVEST_FIRST_NAME=John
2
- HARVEST_LAST_NAME=Smith
1
+ HARVEST_FIRST_NAME=<replace me with a test account>
2
+ HARVEST_LAST_NAME=<replace me with a test account>
3
3
  HARVEST_ACCOUNT_ID=<replace me with a test account>
4
4
  HARVEST_NON_ADMIN_ACCOUNT_ID=<replace me with a test account>
5
5
  HARVEST_ACCESS_TOKEN=<replace me with a test account>
6
6
  HARVEST_NON_ADMIN_ACCESS_TOKEN=<replace me with a test account>
7
7
  HARVEST_ADMIN_FULL_NAME=<replace me with a test account>
8
+ HARVEST_NON_ADMIN_FULL_NAME=<replace me with a test account>
9
+ HARVEST_ADMIN_FIRST_NAME=<replace me with a test account>
10
+ HARVEST_ADMIN_LAST_NAME=<replace me with a test account>
data/.gitignore CHANGED
@@ -9,6 +9,9 @@
9
9
 
10
10
  # environment variables
11
11
  .env
12
+ .env.*
13
+ !.env.sample
14
+
12
15
 
13
16
  # rspec failure tracking
14
17
  .rspec_status
@@ -24,3 +27,5 @@
24
27
 
25
28
  .idea/
26
29
  harvesting.iml
30
+
31
+ .DS_Store
@@ -2,4 +2,8 @@ sudo: false
2
2
  language: ruby
3
3
  rvm:
4
4
  - 2.4.1
5
- before_install: gem install bundler -v 1.16.1
5
+ - 2.5.1
6
+ env:
7
+ - HARVEST_FIRST_NAME=Aaron HARVEST_LAST_NAME=Burr HARVEST_ACCOUNT_ID=112341234 HARVEST_NON_ADMIN_ACCOUNT_ID=112341234 HARVEST_ACCESS_TOKEN=112341234 HARVEST_NON_ADMIN_ACCESS_TOKEN=112341234 HARVEST_ADMIN_FULL_NAME=112341234
8
+
9
+ before_install: gem install bundler -v 1.17.3
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- harvesting (0.2.0)
4
+ harvesting (0.3.0)
5
5
  http (~> 3.3, >= 3.3)
6
6
 
7
7
  GEM
@@ -17,7 +17,7 @@ GEM
17
17
  domain_name (0.5.20180417)
18
18
  unf (>= 0.0.5, < 1.0.0)
19
19
  dotenv (2.5.0)
20
- ffi (1.9.23)
20
+ ffi (1.12.2)
21
21
  formatador (0.2.5)
22
22
  guard (2.14.2)
23
23
  formatador (>= 0.2.4)
@@ -57,7 +57,7 @@ GEM
57
57
  coderay (~> 1.1.0)
58
58
  method_source (~> 0.9.0)
59
59
  public_suffix (3.0.2)
60
- rake (10.5.0)
60
+ rake (13.0.1)
61
61
  rb-fsevent (0.10.3)
62
62
  rb-inotify (0.9.10)
63
63
  ffi (>= 0.5.0, < 2)
@@ -96,10 +96,10 @@ DEPENDENCIES
96
96
  dotenv (~> 2.5, >= 2.5)
97
97
  guard-rspec (~> 4.7, >= 4.7)
98
98
  harvesting!
99
- rake (~> 10.0)
99
+ rake (~> 13.0)
100
100
  rspec (~> 3.0)
101
101
  vcr (~> 4.0, >= 4.0)
102
102
  webmock (~> 3.4, >= 3.4)
103
103
 
104
104
  BUNDLED WITH
105
- 1.16.1
105
+ 1.17.2
data/README.md CHANGED
@@ -1,5 +1,8 @@
1
1
  # Harvesting
2
2
 
3
+ [![Build Status](https://travis-ci.org/ombulabs/harvesting.svg?branch=master)](https://travis-ci.org/ombulabs/harvesting)
4
+ [![Code Climate](https://codeclimate.com/github/ombulabs/harvesting/badges/gpa.svg)](https://codeclimate.com/github/ombulabs/harvesting)
5
+
3
6
  A Ruby gem to interact with the Harvest API v2.0 and forward.
4
7
 
5
8
  ## Installation
@@ -39,69 +42,103 @@ to these environment variables:
39
42
 
40
43
  That means that you could build a client like this:
41
44
 
42
- # $ export HARVEST_ACCESS_TOKEN=abc
43
- # $ export HARVEST_ACCOUNT_ID=12345678
44
- client = Harvesting::Client.new
45
- client.me
46
- > => #<Harvesting::Models::User:0x007ff8830658f0 @attributes={"id"=>2108614, "first_name"=>"Ernesto", "last_name"=>"Tagwerker", ... >
45
+ ```ruby
46
+ # $ export HARVEST_ACCESS_TOKEN=abc
47
+ # $ export HARVEST_ACCOUNT_ID=12345678
48
+ client = Harvesting::Client.new
49
+ client.me
50
+ # => #<Harvesting::Models::User:0x007ff8830658f0 @attributes={"id"=>2108614, "first_name"=>"Ernesto", "last_name"=>"Tagwerker", ... >
51
+ ```
47
52
 
48
53
  If you don't specify a valid combination of token and account id, your code will
49
54
  raise this error:
50
55
 
51
- client = Harvesting::Client.new(access_token: "foo", account_id: "bar")
52
- client.me
53
- > Harvesting::AuthenticationError: {"error":"invalid_token","error_description":"The access token provided is expired, revoked, malformed or invalid for other reasons."}
56
+ ```ruby
57
+ client = Harvesting::Client.new(access_token: "foo", account_id: "bar")
58
+ client.me
59
+ # Harvesting::AuthenticationError: {"error":"invalid_token","error_description":"The access token provided is expired, revoked, malformed or invalid for other reasons."}
60
+ ```
54
61
 
55
62
  If your personal token and account id are valid, you should see something like
56
63
  this:
57
64
 
58
- client = Harvesting::Client.new(access_token: "<your token here>", account_id: "<your account id here>")
59
- user = client.me
60
- > => #<Harvesting::Models::User:0x007ff8830658f0 @attributes={"id"=>2108614, "first_name"=>"Ernesto", "last_name"=>"Tagwerker", ... >
65
+ ```ruby
66
+ client = Harvesting::Client.new(access_token: "<your token here>", account_id: "<your account id here>")
67
+ user = client.me
68
+ # => #<Harvesting::Models::User:0x007ff8830658f0 @attributes={"id"=>2108614, "first_name"=>"Ernesto", "last_name"=>"Tagwerker", ... >
61
69
 
62
- user.id
63
- > => 2108614
70
+ user.id
71
+ # => 2108614
72
+ ```
64
73
 
65
74
  ### Clients
66
75
 
67
- client.clients
68
- > => [#<Harvesting::Models::Client:0x007ff718d65fd0 @attributes={"id"=>6760580, "name"=>"Toto", "is_active"=>true, "address"=>"" ... >
76
+ ```ruby
77
+ client.clients
78
+ # => [#<Harvesting::Models::Client:0x007ff718d65fd0 @attributes={"id"=>6760580, "name"=>"Toto", "is_active"=>true, "address"=>"" ... >
69
79
 
70
- client = client.clients.first
71
- > => #<Harvesting::Models::Client:0x007ff718cf5fc8 @attributes={"id"=>6760580, "name"=>"Toto",
72
- ... >
80
+ client = client.clients.first
81
+ # => #<Harvesting::Models::Client:0x007ff718cf5fc8 @attributes={"id"=>6760580, "name"=>"Toto",
82
+ # ... >
83
+ ```
73
84
 
74
85
  ### Time Entries
75
86
 
76
- time_entries = client.time_entries
77
- > => #<Harvesting::Models::TimeEntries:0x007ff71913e3a0 @attributes={"per_page"=>100, "total_pages"=>1, "total_entries"=>14, "next_page"=>nil, "previous_page"=>nil, "page"=>1, "links"=>{"first"=>"https://api.harvestapp.com/v2/time_entries?limit=1&page=1&per_page=100", "next"=>nil, "previous"=>nil, "last"=>"https://api.harvestapp.com/v2/time_entries?limit=1&page=1&per_page=100"}}, ... >
87
+ ```ruby
88
+ time_entries = client.time_entries
89
+ # => #<Harvesting::Models::TimeEntries:0x007ff71913e3a0 @attributes={"per_page"=>100, "total_pages"=>1, "total_entries"=>14, "next_page"=>nil, "previous_page"=>nil, "page"=>1, "links"=>{"first"=>"https://api.harvestapp.com/v2/time_entries?limit=1&page=1&per_page=100", "next"=>nil, "previous"=>nil, "last"=>"https://api.harvestapp.com/v2/time_entries?limit=1&page=1&per_page=100"}}, ... >
78
90
 
79
- entry = time_entries.first
80
- > => #<Harvesting::Models::TimeEntry:0x007ff71913dfe0 @attributes={"id"=>792860513, "spent_date"=>"2018-05-14", "hours"=>1.0, "notes"=>"hacked the things", "is_locked"=>false, "locked_reason"=>nil, "is_closed"=>false, "is_billed"=>false, "timer_started_at"=>nil, "started_time"=>nil, "ended_time"=>nil, "is_running"=>false, "billable"=>true, "budgeted"=>false, "billable_rate"=>nil, "cost_rate ... >
91
+ entry = time_entries.first
92
+ # => #<Harvesting::Models::TimeEntry:0x007ff71913dfe0 @attributes={"id"=>792860513, "spent_date"=>"2018-05-14", "hours"=>1.0, "notes"=>"hacked the things", "is_locked"=>false, "locked_reason"=>nil, "is_closed"=>false, "is_billed"=>false, "timer_started_at"=>nil, "started_time"=>nil, "ended_time"=>nil, "is_running"=>false, "billable"=>true, "budgeted"=>false, "billable_rate"=>nil, "cost_rate ... >
93
+ ```
81
94
 
82
95
  ### Tasks
83
96
 
84
- tasks = client.tasks
85
- > => #<Harvesting::Models::Tasks:0x007ff718897990 @attributes={"per_page"=>100, "total_pages"=>1, "total_entries"=>6, "next_page"=>nil, "previous_page"=>nil, "page"=>1, "links"=>{"first"=>"https://api.harvestapp.com/v2/tasks?page=1&per_page=100", "next"=>nil, ... >
97
+ ```ruby
98
+ tasks = client.tasks
99
+ # => #<Harvesting::Models::Tasks:0x007ff718897990 @attributes={"per_page"=>100, "total_pages"=>1, "total_entries"=>6, "next_page"=>nil, "previous_page"=>nil, "page"=>1, "links"=>{"first"=>"https://api.harvestapp.com/v2/tasks?page=1&per_page=100", "next"=>nil, ... >
100
+ ```
86
101
 
87
102
  ### Projects
88
103
 
89
- projects = client.projects
90
- > => #<Harvesting::Models::Projects:0x007ff718e1c8e8 @attributes={"per_page"=>100, "total_pages"=>1, "total_entries"=>1, "next_page"=>nil, "previous_page"=>nil, "page"=>1, "links"=>{"first"=>"https://api.harvestapp.com/v2/projects?page=1&per_page=100", ... >
104
+ ```ruby
105
+ projects = client.projects
106
+ # => #<Harvesting::Models::Projects:0x007ff718e1c8e8 @attributes={"per_page"=>100, "total_pages"=>1, "total_entries"=>1, "next_page"=>nil, "previous_page"=>nil, "page"=>1, "links"=>{"first"=>"https://api.harvestapp.com/v2/projects?page=1&per_page=100", ... >
107
+
108
+ project = projects.first
109
+ # => #<Harvesting::Models::Project:0x007ff718e1c618 @attributes={"id"=>17367712, "name"=>"Foo", "code"=>"", "is_active"=>true, "is_billable"=>true, "is_fixed_fee"=>false, "bill_by"=>"none", "budget"=>nil, "budget_by"=>"none", "budget_is_monthly"=>false, "notify_when_over_budget"=>false, "over_budget_notification_percentage"=>80.0, "show_budget_to_all"=>false, "created_at"=>"2018-05-13T03:30:06Z", ... >
110
+ ```
111
+
112
+ ### Invoices
113
+ ```ruby
114
+ invoices = client.invoices
115
+ # => #<Harvesting::Models::Invoices:0x00007fc8905671f0 @models={}, @attributes={"per_page"=>100, "total_pages"=>1, "total_entries"=>3, "next_page"=>nil, "previous_page"=>nil, "page"=>1, "links"=>{"first"=>"https://api.harvestapp.com/v2/invoices?page=1&per_page=100", ... >
116
+
117
+ invoice = invoices.first
118
+ # => #<Harvesting::Models::Invoice:0x00007f8c37eb6d18 @models={}, @attributes={"id"=>23831208, "client_key"=>"73688e97a43ed497ace45939eb76db6b18427b80", "number"=>"3", "purchase_order"=>"", "amount"=>750.0, "due_amount"=>750.0, "tax"=>nil, "tax_amount"=>0.0, "tax2"=>nil, "tax2_amount"=>0.0, "discount"=>nil, "discount_amount"=>0.0, "subject"=>"", "notes"=>"", "state"=>"draft", "period_start"=>nil, ... >
119
+ ```
120
+
121
+ An invoice can have many line items:
122
+ ```ruby
123
+ line_items = invoice.line_items
124
+ # => [#<Harvesting::Models::LineItem:0x00007f92617ce8e0 @models={}, @attributes={"id"=>109677268, "kind"=>"Service", "description"=>"", "quantity"=>3.0, "unit_price"=>250.0, "amount"=>750.0, "taxed"=>false, "taxed2"=>false, "project"=>{"id"=>24566828, "name"=>"Harvest Billing Automation", "code"=>""}}, ... >]
125
+ ```
91
126
 
92
- project = projects.first
93
- > => #<Harvesting::Models::Project:0x007ff718e1c618 @attributes={"id"=>17367712, "name"=>"Foo", "code"=>"", "is_active"=>true, "is_billable"=>true, "is_fixed_fee"=>false, "bill_by"=>"none", "budget"=>nil, "budget_by"=>"none", "budget_is_monthly"=>false, "notify_when_over_budget"=>false, "over_budget_notification_percentage"=>80.0, "show_budget_to_all"=>false, "created_at"=>"2018-05-13T03:30:06Z", ... >
127
+ You can filter invoices by various attributes. E.g. `client.invoices(state: "draft")` only returns invoices in a draft state.
94
128
 
95
129
  ### Nested Attributes
96
130
 
97
131
  The Harvest v2 API embeds some data in JSON objects. You can access nested attributes quite naturally.
98
132
  For example, to access the user id for a time entry instance, `entry`, use:
99
133
 
100
- entry.user.id
134
+ ```ruby
135
+ entry.user.id
136
+ ```
101
137
 
102
138
  Or to access the name of the client on a project instance, `project`:
103
-
104
- project.client.name
139
+ ```ruby
140
+ project.client.name
141
+ ```
105
142
 
106
143
  ## Tips
107
144
 
@@ -109,7 +146,7 @@ Or to access the name of the client on a project instance, `project`:
109
146
 
110
147
  When you need to delete all items, care needs to be taken, because the API uses pagination. The following code will only delete data from _every other_ page.
111
148
 
112
- ```
149
+ ```ruby
113
150
  # WARNING - only deletes every other page
114
151
  client.time_entries.each do |time_entry|
115
152
  time_entry.delete
@@ -120,7 +157,7 @@ While iterating over items from the first page, all of those items will be delet
120
157
 
121
158
  Instead you need to make sure you get access to all of the time entry objects before you try to delete any of them. The easiest way to do this is to convert the `Enumerable` instance into an `Array`, by calling `#to_a`, before you iterate over it.
122
159
 
123
- ```
160
+ ```ruby
124
161
  # GOOD - This should do what you want
125
162
  client.time_entries.to_a.each do |time_entry|
126
163
  time_entry.delete
@@ -1,6 +1,24 @@
1
1
  # RELEASE NOTES
2
2
 
3
- ### Version 0.3.0 - Jan 22, 2018
3
+ ### Version 0.4.0 - June 6, 2020
4
+
5
+ **Notes**
6
+ - Added Ruby 2.5.1 to version matrix in Travis: https://github.com/ombulabs/harvesting/pull/31
7
+ - Associated time entries for project: https://github.com/ombulabs/harvesting/pull/32
8
+ - Add require forwardable in havest_record_collection model: https://github.com/ombulabs/harvesting/pull/40
9
+ - Add syntax highlighting to readme examples: https://github.com/ombulabs/harvesting/pull/41
10
+ - Rename the client key as harvest_client to avoid confusion: https://github.com/ombulabs/harvesting/pull/43
11
+ - Update rake requirement from ~> 10.0 to ~> 13.0: https://github.com/ombulabs/harvesting/pull/44
12
+ - Bump ffi from 1.9.23 to 1.12.2: https://github.com/ombulabs/harvesting/pull/45
13
+ - Ability to supply filter options to the invoice end point and a model for line items on an invoice.: https://github.com/ombulabs/harvesting/pull/46
14
+
15
+ **Bug Fixes**
16
+
17
+ - Complete pending test: https://github.com/ombulabs/harvesting/pull/28
18
+ - Fixed Code Climate link: https://github.com/ombulabs/harvesting/pull/38
19
+
20
+
21
+ ### Version 0.3.0 - Jan 22, 2019
4
22
 
5
23
  **Notes**
6
24
 
@@ -6,8 +6,8 @@ require "harvesting/version"
6
6
  Gem::Specification.new do |spec|
7
7
  spec.name = "harvesting"
8
8
  spec.version = Harvesting::VERSION
9
- spec.authors = ["Ernesto Tagwerker"]
10
- spec.email = ["ernesto+github@ombulabs.com"]
9
+ spec.authors = ["Ernesto Tagwerker", "M. Scott Ford"]
10
+ spec.email = ["ernesto+github@ombulabs.com", "scott@mscottford.com"]
11
11
 
12
12
  spec.summary = %q{Ruby wrapper for the Harvest API v2.0}
13
13
  spec.description = %q{Interact with the Harvest API v2.0 from your Ruby application}
@@ -23,7 +23,7 @@ Gem::Specification.new do |spec|
23
23
  spec.add_dependency "http", "~> 3.3", ">= 3.3"
24
24
 
25
25
  spec.add_development_dependency "bundler", "~> 1.16"
26
- spec.add_development_dependency "rake", "~> 10.0"
26
+ spec.add_development_dependency "rake", "~> 13.0"
27
27
  spec.add_development_dependency "rspec", "~> 3.0"
28
28
  spec.add_development_dependency "guard-rspec", "~> 4.7", ">= 4.7"
29
29
  spec.add_development_dependency "byebug", "~> 10.0", ">= 10.0"
@@ -9,9 +9,11 @@ require "harvesting/models/harvest_record_collection"
9
9
  require "harvesting/models/client"
10
10
  require "harvesting/models/user"
11
11
  require "harvesting/models/project"
12
- require "harvesting/models/invoice"
13
12
  require "harvesting/models/task"
14
- require "harvesting/models/task_assignment"
13
+ require "harvesting/models/project_user_assignment"
14
+ require "harvesting/models/project_task_assignment"
15
+ require "harvesting/models/invoice"
16
+ require "harvesting/models/line_item"
15
17
  require "harvesting/models/time_entry"
16
18
  # harvest record collections
17
19
  require "harvesting/models/tasks"
@@ -19,6 +21,9 @@ require "harvesting/models/users"
19
21
  require "harvesting/models/contact"
20
22
  require "harvesting/models/time_entries"
21
23
  require "harvesting/models/projects"
24
+ require "harvesting/models/project_user_assignments"
25
+ require "harvesting/models/project_task_assignments"
26
+ require "harvesting/models/invoices"
22
27
  # API client
23
28
  require "harvesting/client"
24
29
 
@@ -3,12 +3,20 @@ require "http"
3
3
  require "json"
4
4
 
5
5
  module Harvesting
6
+
7
+ # A client for the Harvest API (version 2.0)
6
8
  class Client
7
9
  DEFAULT_HOST = "https://api.harvestapp.com/v2"
8
10
 
9
11
  attr_accessor :access_token, :account_id
10
12
 
11
- # @param opts
13
+ # Returns a new instance of `Client`
14
+ #
15
+ # client = Client.new(access_token: "12345678", account_id: "98764")
16
+ #
17
+ # @param [Hash] opts the options to create an API client
18
+ # @option opts [String] :access_token Harvest access token
19
+ # @option opts [String] :account_id Harvest account id
12
20
  def initialize(access_token: ENV['HARVEST_ACCESS_TOKEN'], account_id: ENV['HARVEST_ACCOUNT_ID'])
13
21
  @access_token = access_token.to_s
14
22
  @account_id = account_id.to_s
@@ -18,45 +26,68 @@ module Harvesting
18
26
  end
19
27
  end
20
28
 
29
+ # @return [Harvesting::Models::User]
21
30
  def me
22
- Harvesting::Models::User.new(get("users/me"), client: self)
31
+ Harvesting::Models::User.new(get("users/me"), harvest_client: self)
23
32
  end
24
33
 
34
+ # @return [Array<Harvesting::Models::Client>]
25
35
  def clients
26
36
  get("clients")["clients"].map do |result|
27
- Harvesting::Models::Client.new(result, client: self)
37
+ Harvesting::Models::Client.new(result, harvest_client: self)
28
38
  end
29
39
  end
30
40
 
41
+ # @return [Array<Harvesting::Models::Contact>]
31
42
  def contacts
32
43
  get("contacts")["contacts"].map do |result|
33
- Harvesting::Models::Contact.new(result, client: self)
44
+ Harvesting::Models::Contact.new(result, harvest_client: self)
34
45
  end
35
46
  end
36
47
 
48
+ # @return [Harvesting::Models::TimeEntries]
37
49
  def time_entries(opts = {})
38
- Harvesting::Models::TimeEntries.new(get("time_entries", opts), opts, client: self)
50
+ Harvesting::Models::TimeEntries.new(get("time_entries", opts), opts, harvest_client: self)
39
51
  end
40
52
 
53
+ # @return [Harvesting::Models::Projects]
41
54
  def projects(opts = {})
42
- Harvesting::Models::Projects.new(get("projects", opts), opts, client: self)
55
+ Harvesting::Models::Projects.new(get("projects", opts), opts, harvest_client: self)
43
56
  end
44
57
 
58
+ # @return [Harvesting::Models::Tasks]
45
59
  def tasks(opts = {})
46
- Harvesting::Models::Tasks.new(get("tasks", opts), opts, client: self)
60
+ Harvesting::Models::Tasks.new(get("tasks", opts), opts, harvest_client: self)
47
61
  end
48
62
 
49
-
63
+ # @return [Harvesting::Models::Users]
50
64
  def users(opts = {})
51
- Harvesting::Models::Users.new(get("users", opts), opts, client: self)
65
+ Harvesting::Models::Users.new(get("users", opts), opts, harvest_client: self)
52
66
  end
53
67
 
54
- def invoices
55
- get("invoices")["invoices"].map do |result|
56
- Harvesting::Models::Invoice.new(result, client: self)
57
- end
68
+ # @return [Array<Harvesting::Models::Invoice>]
69
+ def invoices(opts = {})
70
+ Harvesting::Models::Invoices.new(get("invoices", opts), opts, harvest_client: self)
71
+ end
72
+
73
+ # @return [Harvesting::Models::ProjectUserAssignments]
74
+ def user_assignments(opts = {})
75
+ project_id = opts.delete(:project_id)
76
+ path = project_id.nil? ? "user_assignments" : "projects/#{project_id}/user_assignments"
77
+ Harvesting::Models::ProjectUserAssignments.new(get(path, opts), opts, harvest_client: self)
78
+ end
79
+
80
+ # @return [Harvesting::Models::ProjectTaskAssignments]
81
+ def task_assignments(opts = {})
82
+ project_id = opts.delete(:project_id)
83
+ path = project_id.nil? ? "task_assignments" : "projects/#{project_id}/task_assignments"
84
+ Harvesting::Models::ProjectTaskAssignments.new(get(path, opts), opts, harvest_client: self)
58
85
  end
59
86
 
87
+ # Creates an `entity` in your Harvest account.
88
+ #
89
+ # @param entity [Harvesting::Models::Base] A new record in your Harvest account
90
+ # @return [Harvesting::Models::Base] A subclass of `Harvesting::Models::Base` updated with the response from Harvest
60
91
  def create(entity)
61
92
  url = "#{DEFAULT_HOST}/#{entity.path}"
62
93
  uri = URI(url)
@@ -65,6 +96,10 @@ module Harvesting
65
96
  entity
66
97
  end
67
98
 
99
+ # Updates an `entity` in your Harvest account.
100
+ #
101
+ # @param entity [Harvesting::Models::Base] An existing record in your Harvest account
102
+ # @return [Harvesting::Models::Base] A subclass of `Harvesting::Models::Base` updated with the response from Harvest
68
103
  def update(entity)
69
104
  url = "#{DEFAULT_HOST}/#{entity.path}"
70
105
  uri = URI(url)
@@ -73,13 +108,24 @@ module Harvesting
73
108
  entity
74
109
  end
75
110
 
111
+ # It removes an `entity` from your Harvest account.
112
+ #
113
+ # @param entity [Harvesting::Models::Base] A record to be removed from your Harvest account
114
+ # @return [Hash]
115
+ # @raise [UnprocessableRequest] When HTTP response is not 200 OK
76
116
  def delete(entity)
77
117
  url = "#{DEFAULT_HOST}/#{entity.path}"
78
118
  uri = URI(url)
79
119
  response = http_response(:delete, uri)
80
120
  raise UnprocessableRequest(response.to_s) unless response.code.to_i == 200
121
+ JSON.parse(response.body)
81
122
  end
82
123
 
124
+ # Performs a GET request and returned the parsed JSON as a Hash.
125
+ #
126
+ # @param path [String] path to be combined with `DEFAULT_HOST`
127
+ # @param opts [Hash] key/values will get passed as HTTP (GET) parameters
128
+ # @return [Hash]
83
129
  def get(path, opts = {})
84
130
  url = "#{DEFAULT_HOST}/#{path}"
85
131
  url += "?#{opts.map {|k, v| "#{k}=#{v}"}.join("&")}" if opts.any?
@@ -1,3 +1,6 @@
1
+ # `Enumerable` extends the stdlib `Enumerable` to provide pagination for paged
2
+ # API requests.
3
+ #
1
4
  # @see https://github.com/sferik/twitter/blob/aa909b3b7733ca619d80f1c8cba961033d1fc7e6/lib/twitter/enumerable.rb
2
5
  module Harvesting
3
6
  module Enumerable
@@ -1,54 +1,62 @@
1
1
  module Harvesting
2
2
  module Models
3
3
  class Base
4
+ # @return [Hash]
4
5
  attr_accessor :attributes
6
+ # @return [Harvesting::Model::Client]
5
7
  attr_reader :harvest_client
6
8
 
7
9
  def initialize(attrs, opts = {})
8
10
  @models = {}
9
11
  @attributes = attrs.dup
10
- @harvest_client = opts[:client] || Harvesting::Client.new(opts)
11
- end
12
-
13
- def self.attributed(*attribute_names)
14
- attribute_names.each do |attribute_name|
15
- define_method(attribute_name) do
16
- @attributes[__method__.to_s]
17
- end
18
- end
19
- end
20
-
21
- def self.modeled(opts = {})
22
- opts.each do |attribute_name, model|
23
- attribute_name_string = attribute_name.to_s
24
- Harvesting::Models::Base.send :define_method, attribute_name_string do
25
- @models[attribute_name_string] ||= model.new(@attributes[attribute_name_string] || {}, client: harvest_client)
26
- end
27
- end
12
+ @harvest_client = opts[:harvest_client] || Harvesting::Client.new(opts)
28
13
  end
29
14
 
15
+ # It calls `create` or `update` depending on the record's ID. If the ID
16
+ # is present, then it calls `update`. Otherwise it calls `create`
17
+ #
18
+ # @see Client#create
19
+ # @see Client#update
30
20
  def save
31
21
  id.nil? ? create : update
32
22
  end
33
23
 
24
+ # It creates the record.
25
+ #
26
+ # @see Client#create
27
+ # @return [Harvesting::Models::Base]
34
28
  def create
35
29
  @harvest_client.create(self)
36
30
  end
37
31
 
32
+ # It updates the record.
33
+ #
34
+ # @see Client#update
35
+ # @return [Harvesting::Models::Base]
38
36
  def update
39
37
  @harvest_client.update(self)
40
38
  end
41
39
 
40
+ # It removes the record.
41
+ #
42
+ # @see Client#delete
43
+ # @return [Harvesting::Models::Base]
42
44
  def delete
43
45
  @harvest_client.delete(self)
44
46
  end
45
47
 
48
+ # It returns keys and values for all the attributes of this record.
49
+ #
50
+ # @return [Hash]
46
51
  def to_hash
47
52
  @attributes
48
53
  end
49
54
 
55
+ # It loads a new record from your Harvest account.
56
+ #
57
+ # @return [Harvesting::Models::Base]
50
58
  def fetch
51
- self.class.new(@harvest_client.get(path), client: @harvest_client)
59
+ self.class.new(@harvest_client.get(path), harvest_client: @harvest_client)
52
60
  end
53
61
 
54
62
  # Retrieves an instance of the object by ID
@@ -57,9 +65,54 @@ module Harvesting
57
65
  # @param opts [Hash] options to pass along to the `Harvesting::Client`
58
66
  # instance
59
67
  def self.get(id, opts = {})
60
- client = opts[:client] || Harvesting::Client.new(opts)
68
+ client = opts[:harvest_client] || Harvesting::Client.new(opts)
61
69
  self.new({ 'id' => id }, opts).fetch
62
70
  end
71
+
72
+ protected
73
+
74
+ # Class method to define attribute methods for accessing attributes for
75
+ # a record
76
+ #
77
+ # It needs to be used like this:
78
+ #
79
+ # class Contact < HarvestRecord
80
+ # attributed :id,
81
+ # :title,
82
+ # :first_name
83
+ # ...
84
+ # end
85
+ #
86
+ # @param attribute_names [Array] A list of attributes
87
+ def self.attributed(*attribute_names)
88
+ attribute_names.each do |attribute_name|
89
+ define_method(attribute_name) do
90
+ @attributes[__method__.to_s]
91
+ end
92
+ define_method("#{attribute_name}=") do |value|
93
+ @attributes[__method__.to_s.chop] = value
94
+ end
95
+ end
96
+ end
97
+
98
+ # Class method to define nested resources for a record.
99
+ #
100
+ # It needs to be used like this:
101
+ #
102
+ # class Contact < HarvestRecord
103
+ # modeled client: Client
104
+ # ...
105
+ # end
106
+ #
107
+ # @param opts [Hash] key = symbol that needs to be the same as the one returned by the Harvest API. value = model class for the nested resource.
108
+ def self.modeled(opts = {})
109
+ opts.each do |attribute_name, model|
110
+ attribute_name_string = attribute_name.to_s
111
+ Harvesting::Models::Base.send :define_method, attribute_name_string do
112
+ @models[attribute_name_string] ||= model.new(@attributes[attribute_name_string] || {}, harvest_client: harvest_client)
113
+ end
114
+ end
115
+ end
63
116
  end
64
117
  end
65
118
  end
@@ -1,5 +1,8 @@
1
1
  module Harvesting
2
2
  module Models
3
+ # A client record from your Harvest account.
4
+ #
5
+ # For more information: https://help.getharvest.com/api-v2/clients-api/clients/clients/
3
6
  class Client < HarvestRecord
4
7
  attributed :id,
5
8
  :name,
@@ -1,5 +1,8 @@
1
1
  module Harvesting
2
2
  module Models
3
+ # A contact record from your Harvest account.
4
+ #
5
+ # For more information: https://help.getharvest.com/api-v2/clients-api/clients/contacts/
3
6
  class Contact < HarvestRecord
4
7
  attributed :id,
5
8
  :title,
@@ -17,7 +20,7 @@ module Harvesting
17
20
  def path
18
21
  @attributes['id'].nil? ? "contacts" : "contacts/#{@attributes['id']}"
19
22
  end
20
-
23
+
21
24
  def to_hash
22
25
  { client_id: client.id }.merge(super)
23
26
  end
@@ -1,3 +1,5 @@
1
+ require 'forwardable'
2
+
1
3
  module Harvesting
2
4
  module Models
3
5
  class HarvestRecordCollection < Base
@@ -1,9 +1,11 @@
1
1
  module Harvesting
2
2
  module Models
3
+ # An invoice record from your Harvest account.
4
+ #
5
+ # For more information: https://help.getharvest.com/api-v2/invoices-api/invoices/invoices/
3
6
  class Invoice < HarvestRecord
4
7
  attributed :id,
5
8
  :client_key,
6
- :line_items,
7
9
  :number,
8
10
  :purchase_order,
9
11
  :amount,
@@ -29,6 +31,11 @@ module Harvesting
29
31
  :closed_at,
30
32
  :created_at,
31
33
  :updated_at
34
+
35
+ def line_items
36
+ @line_items ||= @attributes['line_items'].map { |line_item_attributes| LineItem.new line_item_attributes, { harvest_client: harvest_client } }
37
+ end
38
+
32
39
  def path
33
40
  @attributes['id'].nil? ? "invoices" : "invoices/#{@attributes['id']}"
34
41
  end
@@ -0,0 +1,17 @@
1
+ module Harvesting
2
+ module Models
3
+ class Invoices < HarvestRecordCollection
4
+ def initialize(attrs, query_opts = {}, opts = {})
5
+ super(attrs.reject {|k,v| k == "invoices" }, query_opts, opts)
6
+ @entries = attrs["invoices"].map do |entry|
7
+ Invoice.new(entry, harvest_client: opts[:harvest_client])
8
+ end
9
+ end
10
+
11
+ def fetch_next_page
12
+ @entries += harvest_client.invoices(next_page_query_opts).entries
13
+ @attributes['page'] = page + 1
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,19 @@
1
+ module Harvesting
2
+ module Models
3
+ # A line item on an invoice from your Harvest account.
4
+ #
5
+ # For more information: https://help.getharvest.com/api-v2/invoices-api/invoices/invoices/
6
+ class LineItem < HarvestRecord
7
+ attributed :id,
8
+ :kind,
9
+ :description,
10
+ :quantity,
11
+ :unit_price,
12
+ :amount,
13
+ :taxed,
14
+ :taxed2
15
+
16
+ modeled project: Project
17
+ end
18
+ end
19
+ end
@@ -1,5 +1,8 @@
1
1
  module Harvesting
2
2
  module Models
3
+ # A project record from your Harvest account.
4
+ #
5
+ # For more information: https://help.getharvest.com/api-v2/projects-api/projects/projects/
3
6
  class Project < HarvestRecord
4
7
  attributed :id,
5
8
  :name,
@@ -30,10 +33,26 @@ module Harvesting
30
33
  def path
31
34
  @attributes['id'].nil? ? "projects" : "projects/#{@attributes['id']}"
32
35
  end
33
-
36
+
34
37
  def to_hash
35
38
  { client_id: client.id }.merge(super)
36
39
  end
40
+
41
+ def time_entries
42
+ harvest_client.time_entries(project_id: self.id)
43
+ end
44
+
45
+ # Provides access to the user assignments that are associated with this
46
+ # project.
47
+ def user_assignments
48
+ harvest_client.user_assignments(project_id: self.id)
49
+ end
50
+
51
+ # Provides access to the task assignments that are associated with this
52
+ # project.
53
+ def task_assignments
54
+ harvest_client.task_assignments(project_id: self.id)
55
+ end
37
56
  end
38
57
  end
39
58
  end
@@ -1,6 +1,9 @@
1
1
  module Harvesting
2
2
  module Models
3
- class TaskAssignment < HarvestRecord
3
+ # A task assignment record from your Harvest account.
4
+ #
5
+ # For more information: https://help.getharvest.com/api-v2/projects-api/projects/task-assignments/
6
+ class ProjectTaskAssignment < HarvestRecord
4
7
  attributed :id,
5
8
  :is_active,
6
9
  :billable,
@@ -21,11 +24,10 @@ module Harvesting
21
24
  # # TODO: handle case where project's id is part of json object
22
25
  # @attributes["project_id"]
23
26
  # end
24
-
27
+
25
28
  def to_hash
26
29
  { project_id: project.id, task_id: task.id }.merge(super)
27
30
  end
28
-
29
31
  end
30
32
  end
31
33
  end
@@ -0,0 +1,18 @@
1
+ module Harvesting
2
+ module Models
3
+ class ProjectTaskAssignments < HarvestRecordCollection
4
+ def initialize(attrs, query_opts = {}, opts = {})
5
+ super(attrs.reject {|k,v| k == "task_assignments" }, query_opts, opts)
6
+ @entries = attrs["task_assignments"].map do |entry|
7
+ ProjectTaskAssignment.new(entry, harvest_client: opts[:harvest_client])
8
+ end
9
+ end
10
+
11
+ def fetch_next_page
12
+ @entries += harvest_client.task_assignments(next_page_query_opts).entries
13
+ @attributes['page'] = page + 1
14
+ end
15
+
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,25 @@
1
+ module Harvesting
2
+ module Models
3
+ class ProjectUserAssignment < HarvestRecord
4
+ attributed :id,
5
+ :is_active,
6
+ :is_project_manager,
7
+ :hourly_rate,
8
+ :budget,
9
+ :created_at,
10
+ :updated_at
11
+
12
+ modeled project: Project,
13
+ user: User
14
+
15
+ def path
16
+ base_url = "projects/#{project.id}/user_assignments"
17
+ @attributes['id'].nil? ? base_url : "#{base_url}/#{@attributes['id']}"
18
+ end
19
+
20
+ def to_hash
21
+ { project_id: project.id, user_id: user.id }.merge(super)
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,19 @@
1
+ module Harvesting
2
+ module Models
3
+ class ProjectUserAssignments < HarvestRecordCollection
4
+
5
+ def initialize(attrs, query_opts = {}, opts = {})
6
+ super(attrs.reject {|k,v| k == "user_assignments" }, query_opts, opts)
7
+ @entries = attrs["user_assignments"].map do |entry|
8
+ ProjectUserAssignment.new(entry, harvest_client: opts[:harvest_client])
9
+ end
10
+ end
11
+
12
+ def fetch_next_page
13
+ @entries += harvest_client.user_assignments(next_page_query_opts).entries
14
+ @attributes['page'] = page + 1
15
+ end
16
+
17
+ end
18
+ end
19
+ end
@@ -5,7 +5,7 @@ module Harvesting
5
5
  def initialize(attrs, query_opts = {}, opts = {})
6
6
  super(attrs.reject {|k,v| k == "projects" }, query_opts, opts)
7
7
  @entries = attrs["projects"].map do |entry|
8
- Project.new(entry, client: opts[:client])
8
+ Project.new(entry, harvest_client: opts[:harvest_client])
9
9
  end
10
10
  end
11
11
 
@@ -1,9 +1,12 @@
1
1
  module Harvesting
2
2
  module Models
3
+ # A task record from your Harvest account.
4
+ #
5
+ # For more information: https://help.getharvest.com/api-v2/tasks-api/tasks/tasks/
3
6
  class Task < HarvestRecord
4
7
  attributed :id,
5
8
  :name,
6
- :billable_by_default,
9
+ :billable_by_default,
7
10
  :default_hourly_rate,
8
11
  :is_default,
9
12
  :is_active,
@@ -5,7 +5,7 @@ module Harvesting
5
5
  def initialize(attrs, query_opts = {}, opts = {})
6
6
  super(attrs.reject {|k,v| k == "tasks" }, query_opts, opts)
7
7
  @entries = attrs["tasks"].map do |entry|
8
- Task.new(entry, client: opts[:client])
8
+ Task.new(entry, harvest_client: opts[:harvest_client])
9
9
  end
10
10
  end
11
11
 
@@ -5,7 +5,7 @@ module Harvesting
5
5
  def initialize(attrs, query_opts = {}, opts = {})
6
6
  super(attrs.reject {|k,v| k == "time_entries" }, query_opts, opts)
7
7
  @entries = attrs["time_entries"].map do |entry|
8
- TimeEntry.new(entry, client: opts[:client])
8
+ TimeEntry.new(entry, harvest_client: opts[:harvest_client])
9
9
  end
10
10
  end
11
11
 
@@ -1,5 +1,8 @@
1
1
  module Harvesting
2
2
  module Models
3
+ # A time entry record from your Harvest account.
4
+ #
5
+ # For more information: https://help.getharvest.com/api-v2/timesheets-api/timesheets/time-entries/
3
6
  class TimeEntry < HarvestRecord
4
7
  attributed :id,
5
8
  :spent_date,
@@ -20,14 +23,14 @@ module Harvesting
20
23
  :invoice,
21
24
  :external_reference,
22
25
  :created_at,
23
- :updated_at,
24
- :user_assignment # temporarily return the hash itself until the model is added
26
+ :updated_at
25
27
 
26
28
  modeled project: Project,
27
29
  user: User,
28
30
  task: Task,
29
31
  client: Client,
30
- task_assignment: TaskAssignment
32
+ task_assignment: ProjectTaskAssignment,
33
+ user_assignment: ProjectUserAssignment
31
34
 
32
35
 
33
36
  def path
@@ -1,7 +1,11 @@
1
1
  module Harvesting
2
2
  module Models
3
+ # An user record from your Harvest account.
4
+ #
5
+ # For more information: https://help.getharvest.com/api-v2/users-api/users/users/
3
6
  class User < HarvestRecord
4
7
  attributed :id,
8
+ :name,
5
9
  :first_name,
6
10
  :last_name,
7
11
  :email,
@@ -26,6 +30,10 @@ module Harvesting
26
30
  def path
27
31
  @attributes['id'].nil? ? "users" : "users/#{@attributes['id']}"
28
32
  end
33
+
34
+ def name
35
+ @attributes['name'].nil? ? "#{first_name} #{last_name}" : @attributes['name']
36
+ end
29
37
  end
30
38
  end
31
39
  end
@@ -5,7 +5,7 @@ module Harvesting
5
5
  def initialize(attrs, query_opts = {}, opts = {})
6
6
  super(attrs.reject {|k,v| k == "users" }, query_opts, opts)
7
7
  @entries = attrs["users"].map do |entry|
8
- User.new(entry, client: opts[:client])
8
+ User.new(entry, harvest_client: opts[:harvest_client])
9
9
  end
10
10
  end
11
11
 
@@ -1,3 +1,3 @@
1
1
  module Harvesting
2
- VERSION = "0.3.0".freeze
2
+ VERSION = "0.4.0".freeze
3
3
  end
metadata CHANGED
@@ -1,14 +1,15 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: harvesting
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ernesto Tagwerker
8
+ - M. Scott Ford
8
9
  autorequire:
9
10
  bindir: exe
10
11
  cert_chain: []
11
- date: 2019-01-22 00:00:00.000000000 Z
12
+ date: 2020-06-06 00:00:00.000000000 Z
12
13
  dependencies:
13
14
  - !ruby/object:Gem::Dependency
14
15
  name: http
@@ -50,14 +51,14 @@ dependencies:
50
51
  requirements:
51
52
  - - "~>"
52
53
  - !ruby/object:Gem::Version
53
- version: '10.0'
54
+ version: '13.0'
54
55
  type: :development
55
56
  prerelease: false
56
57
  version_requirements: !ruby/object:Gem::Requirement
57
58
  requirements:
58
59
  - - "~>"
59
60
  - !ruby/object:Gem::Version
60
- version: '10.0'
61
+ version: '13.0'
61
62
  - !ruby/object:Gem::Dependency
62
63
  name: rspec
63
64
  requirement: !ruby/object:Gem::Requirement
@@ -175,6 +176,7 @@ dependencies:
175
176
  description: Interact with the Harvest API v2.0 from your Ruby application
176
177
  email:
177
178
  - ernesto+github@ombulabs.com
179
+ - scott@mscottford.com
178
180
  executables: []
179
181
  extensions: []
180
182
  extra_rdoc_files: []
@@ -206,10 +208,15 @@ files:
206
208
  - lib/harvesting/models/harvest_record.rb
207
209
  - lib/harvesting/models/harvest_record_collection.rb
208
210
  - lib/harvesting/models/invoice.rb
211
+ - lib/harvesting/models/invoices.rb
212
+ - lib/harvesting/models/line_item.rb
209
213
  - lib/harvesting/models/project.rb
214
+ - lib/harvesting/models/project_task_assignment.rb
215
+ - lib/harvesting/models/project_task_assignments.rb
216
+ - lib/harvesting/models/project_user_assignment.rb
217
+ - lib/harvesting/models/project_user_assignments.rb
210
218
  - lib/harvesting/models/projects.rb
211
219
  - lib/harvesting/models/task.rb
212
- - lib/harvesting/models/task_assignment.rb
213
220
  - lib/harvesting/models/tasks.rb
214
221
  - lib/harvesting/models/time_entries.rb
215
222
  - lib/harvesting/models/time_entry.rb
@@ -235,8 +242,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
235
242
  - !ruby/object:Gem::Version
236
243
  version: '0'
237
244
  requirements: []
238
- rubyforge_project:
239
- rubygems_version: 2.7.8
245
+ rubygems_version: 3.0.8
240
246
  signing_key:
241
247
  specification_version: 4
242
248
  summary: Ruby wrapper for the Harvest API v2.0