harvesting 0.2.0 → 0.3.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: 1673fee5f8b50a00750256df7dea9b8143b4afbead80fbdf431cd2ecd19c6a1c
4
- data.tar.gz: 5cb711e920654119f73ade9d2a9cc32a71ff869630b9a6d2ee0f48003cd743b5
3
+ metadata.gz: 420911f1f064b300fb9db0e3490a23cee246fd1e8f885ac631558818a9afa761
4
+ data.tar.gz: cbf6caa05a4092e344e1638dab33081478a973282d3d1a9f2ed3cb30ec75f7ab
5
5
  SHA512:
6
- metadata.gz: 36d93fb5fdea77d42429d5e9116a12409e480746a261ce6f615987fa2c47b04e13f95a160eebcdea0314df78dd937b9353d8bafb21d8f44c8e64fa3f72928913
7
- data.tar.gz: 3ba6fa74a9e549fb2bfb49f4cbde3e6b7c46fcb30564dbe9d0da14d1b0f03e8bd14e1f018b43c0667888f42b82c99ea89fb78da91c647039e9b5e616fe7c3486
6
+ metadata.gz: 3b9a22eb96ff64f1f0947b22d026311635419b29616e56a01baca381fac7ff0d61cbe1ff29597d24d461fb1a58d92db4edf755ac7c5ac206b71002e5bc3442f7
7
+ data.tar.gz: 398e22c481a9f56e543beebb87a0b0572e725e8f2dd5eb61ea78176bec5f56d6afac652e1a9477754eb608fc612fe45d212fed8fb2034e1724997cce017855a1
@@ -0,0 +1,7 @@
1
+ HARVEST_FIRST_NAME=John
2
+ HARVEST_LAST_NAME=Smith
3
+ HARVEST_ACCOUNT_ID=<replace me with a test account>
4
+ HARVEST_NON_ADMIN_ACCOUNT_ID=<replace me with a test account>
5
+ HARVEST_ACCESS_TOKEN=<replace me with a test account>
6
+ HARVEST_NON_ADMIN_ACCESS_TOKEN=<replace me with a test account>
7
+ HARVEST_ADMIN_FULL_NAME=<replace me with a test account>
data/.gitignore CHANGED
@@ -7,6 +7,9 @@
7
7
  /spec/reports/
8
8
  /tmp/
9
9
 
10
+ # environment variables
11
+ .env
12
+
10
13
  # rspec failure tracking
11
14
  .rspec_status
12
15
 
@@ -18,3 +21,6 @@
18
21
 
19
22
  # ruby versions
20
23
  .ruby-version
24
+
25
+ .idea/
26
+ harvesting.iml
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- harvesting (0.1.0)
4
+ harvesting (0.2.0)
5
5
  http (~> 3.3, >= 3.3)
6
6
 
7
7
  GEM
@@ -16,6 +16,7 @@ GEM
16
16
  diff-lcs (1.3)
17
17
  domain_name (0.5.20180417)
18
18
  unf (>= 0.0.5, < 1.0.0)
19
+ dotenv (2.5.0)
19
20
  ffi (1.9.23)
20
21
  formatador (0.2.5)
21
22
  guard (2.14.2)
@@ -92,6 +93,7 @@ PLATFORMS
92
93
  DEPENDENCIES
93
94
  bundler (~> 1.16)
94
95
  byebug (~> 10.0, >= 10.0)
96
+ dotenv (~> 2.5, >= 2.5)
95
97
  guard-rspec (~> 4.7, >= 4.7)
96
98
  harvesting!
97
99
  rake (~> 10.0)
data/README.md CHANGED
@@ -92,6 +92,41 @@ this:
92
92
  project = projects.first
93
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", ... >
94
94
 
95
+ ### Nested Attributes
96
+
97
+ The Harvest v2 API embeds some data in JSON objects. You can access nested attributes quite naturally.
98
+ For example, to access the user id for a time entry instance, `entry`, use:
99
+
100
+ entry.user.id
101
+
102
+ Or to access the name of the client on a project instance, `project`:
103
+
104
+ project.client.name
105
+
106
+ ## Tips
107
+
108
+ ### Deleting All Items
109
+
110
+ 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
+
112
+ ```
113
+ # WARNING - only deletes every other page
114
+ client.time_entries.each do |time_entry|
115
+ time_entry.delete
116
+ end
117
+ ```
118
+
119
+ While iterating over items from the first page, all of those items will be deleted. This will result in moving items from the second page onto the first page. If there are only two pages, then the second page will be empty. If there are more than two pages, then the second page will now contain items which would have previously appeared on the third page. Deleting those items will move the items from the fourth page on to the third page, and so on.
120
+
121
+ 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
+
123
+ ```
124
+ # GOOD - This should do what you want
125
+ client.time_entries.to_a.each do |time_entry|
126
+ time_entry.delete
127
+ end
128
+ ```
129
+
95
130
  ## Roadmap
96
131
 
97
132
  There are many things to be developed for this gem. For now they are tracked here: [TODO.md](https://github.com/ombulabs/harvesting/blob/master/TODO.md)
@@ -102,21 +137,73 @@ You can find more info [here](RELEASE_NOTES.md)
102
137
 
103
138
  ## Development
104
139
 
105
- ### Without Docker
106
-
107
140
  After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
108
141
 
109
- To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
142
+ ./bin/setup
143
+ rake
110
144
 
111
- ### With Docker
145
+ To install this gem onto your local machine, run `bundle exec rake install`.
146
+
147
+ ### Using Docker
112
148
 
113
149
  This setup allows you to create a completely isolated development environment for working on this gem. The version of ruby used for development and the gems that it depends on will remain inside the container.
114
150
 
115
151
  After checking out the repo, run `docker-compose build` to create the `gem` container. Running `docker-compose run gem bash` will get you a bash session inside the container. From there, you can follow the instructions in the "Without Docker" section above.
116
152
 
153
+ ### Running Tests
154
+
155
+ The tests in this project use the [VCR gem](https://github.com/vcr/vcr) to record and playback all interactions with the Harvest API. This allows you to run the test suite without having an account at Harvest for testing.
156
+
157
+ If you add a test that requires making an additional API call, then you'll need to make adjustments to the `.env` file to provide account details that are required by the test suite.
158
+
159
+ *WARNING*: The test suite is destructive. It deletes all entities from the account before it runs. _DO NOT_ run it against a Harvest account which contains information that you need to preserve. Instead, create a Harvest account for testing.
160
+
161
+ If you need to refresh the VCR cassettes, the easiest way is to delete all of the files located under [`fixtures/vcr_cassettes`](fixtures/vcr_cassettes). The next time the test suite is run, VCR will make actual calls against the Harvest API and record the responses into updated cassette files.
162
+
163
+ Effort has been taken to ensure that private information is excluded from the recorded cassettes. To adjust this further, add additional `filter_sensitive_data` calls to [`spec/spec_helper.rb`](spec/spec_helper.rb).
164
+
165
+ ### Models
166
+
167
+ The models in this project reflect the Harvest v2 API endpoints:
168
+
169
+ * client
170
+ * contact
171
+ * task
172
+ * task_assignment
173
+ * project
174
+ * user
175
+ * time_entry
176
+
177
+ There are also models for the Harvest v2 API collection endpoints:
178
+
179
+ * clients
180
+ * contacts
181
+ * tasks
182
+ * task_assignments
183
+ * projects
184
+ * users
185
+ * time_entries
186
+
187
+ These collection models handle the Harvest v2 API pagination automatically, making it easy to enumerate through all the instances of each type.
188
+
189
+ The models try to reduce code duplication through base class helper functions to automatically define accessors for the attributes included in each type returned by the Harvest v2 API.
190
+ The `Harvesting::Base::attributed` method will define accessors for each simple attribute included in an array passed as an argument. Data is returned from these accessors as strings.
191
+
192
+ Some data is returned from Harvest as nested JSON (e.g. time_entry.project.name). A base class helper to expose this using the available models
193
+ is also present. The `Harvesting::Base::modeled` method will define accessors for each object attribute included in options. Both the name of the attribute and the model to use in accessing that data is supplied.
194
+ Data is returned from these accessors as model objects.
195
+
196
+ NOTE: Nesting model objects requires that the nested model types be defined before the nesting model type. E.g. if `project` contains a nested `client`, then `client` must be defined *before* `project` in the `harvesting.rb` include list.
197
+
198
+ ## Releases
199
+
200
+ To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
201
+
202
+ We follow semantic versioning for version numbers: [https://semver.org](https://semver.org)
203
+
117
204
  ## Contributing
118
205
 
119
- Bug reports and pull requests are welcome on GitHub at https://github.com/ombulabs/harvesting. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
206
+ Bug reports and pull requests are welcome on GitHub at [https://github.com/ombulabs/harvesting](https://github.com/ombulabs/harvesting). This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
120
207
 
121
208
  ## License
122
209
 
@@ -1,10 +1,18 @@
1
1
  # RELEASE NOTES
2
2
 
3
- ## Version 0.1.0 - Sep 04, 2018
3
+ ### Version 0.3.0 - Jan 22, 2018
4
4
 
5
5
  **Notes**
6
6
 
7
- - Initial release
7
+ - Support for users: https://github.com/ombulabs/harvesting/pull/9
8
+ - Support for fetching single records: https://github.com/ombulabs/harvesting/pull/14 and https://github.com/ombulabs/harvesting/pull/22
9
+ - Nested models make it easier to access data: https://github.com/ombulabs/harvesting/pull/15
10
+ - Better architecture for collections: https://github.com/ombulabs/harvesting/pull/18
11
+
12
+ **Bug Fixes**
13
+
14
+ - Correct pagination support: https://github.com/ombulabs/harvesting/pull/17
15
+ - Added documentation: https://github.com/ombulabs/harvesting/pull/30
8
16
 
9
17
  ### Version 0.2.0 - Oct 18, 2018
10
18
 
@@ -18,3 +26,9 @@
18
26
 
19
27
  - Fixes issues with specs: https://github.com/ombulabs/harvesting/pull/2
20
28
  - Fixed https://github.com/ombulabs/harvesting/issues/6 with https://github.com/ombulabs/harvesting/pull/7
29
+
30
+ ## Version 0.1.0 - Sep 04, 2018
31
+
32
+ **Notes**
33
+
34
+ - Initial release
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
3
  require "bundler/setup"
4
+ require "dotenv/load"
4
5
  require "harvesting"
5
6
 
6
7
  # You can add fixtures and/or initialization code here to make experimenting
data/bin/setup CHANGED
@@ -6,3 +6,8 @@ set -vx
6
6
  bundle install
7
7
 
8
8
  # Do any other automated setup that you need to do here
9
+
10
+ if [ ! -f .env ]; then
11
+ cp .env.sample .env
12
+ echo "Make sure that .env has valid values."
13
+ fi
@@ -29,4 +29,5 @@ Gem::Specification.new do |spec|
29
29
  spec.add_development_dependency "byebug", "~> 10.0", ">= 10.0"
30
30
  spec.add_development_dependency "vcr", "~> 4.0", ">= 4.0"
31
31
  spec.add_development_dependency "webmock", "~> 3.4", ">= 3.4"
32
+ spec.add_development_dependency "dotenv", "~> 2.5", ">= 2.5"
32
33
  end
@@ -1,16 +1,25 @@
1
+ # framework
1
2
  require "harvesting/version"
2
3
  require "harvesting/enumerable"
3
4
  require "harvesting/errors"
4
5
  require "harvesting/models/base"
5
- require "harvesting/models/user"
6
- require "harvesting/models/contact"
6
+ require "harvesting/models/harvest_record"
7
+ require "harvesting/models/harvest_record_collection"
8
+ # harvest records
7
9
  require "harvesting/models/client"
8
- require "harvesting/models/projects"
10
+ require "harvesting/models/user"
9
11
  require "harvesting/models/project"
10
- require "harvesting/models/tasks"
12
+ require "harvesting/models/invoice"
11
13
  require "harvesting/models/task"
12
- require "harvesting/models/time_entries"
14
+ require "harvesting/models/task_assignment"
13
15
  require "harvesting/models/time_entry"
16
+ # harvest record collections
17
+ require "harvesting/models/tasks"
18
+ require "harvesting/models/users"
19
+ require "harvesting/models/contact"
20
+ require "harvesting/models/time_entries"
21
+ require "harvesting/models/projects"
22
+ # API client
14
23
  require "harvesting/client"
15
24
 
16
25
  module Harvesting
@@ -1,5 +1,4 @@
1
1
  # frozen_string_literal: true
2
-
3
2
  require "http"
4
3
  require "json"
5
4
 
@@ -9,9 +8,7 @@ module Harvesting
9
8
 
10
9
  attr_accessor :access_token, :account_id
11
10
 
12
- #
13
11
  # @param opts
14
- #
15
12
  def initialize(access_token: ENV['HARVEST_ACCESS_TOKEN'], account_id: ENV['HARVEST_ACCOUNT_ID'])
16
13
  @access_token = access_token.to_s
17
14
  @account_id = account_id.to_s
@@ -38,15 +35,26 @@ module Harvesting
38
35
  end
39
36
 
40
37
  def time_entries(opts = {})
41
- Harvesting::Models::TimeEntries.new(get("time_entries", opts), client: self)
38
+ Harvesting::Models::TimeEntries.new(get("time_entries", opts), opts, client: self)
42
39
  end
43
40
 
44
41
  def projects(opts = {})
45
- Harvesting::Models::Projects.new(get("projects", opts), client: self)
42
+ Harvesting::Models::Projects.new(get("projects", opts), opts, client: self)
46
43
  end
47
44
 
48
45
  def tasks(opts = {})
49
- Harvesting::Models::Tasks.new(get("tasks", opts), client: self)
46
+ Harvesting::Models::Tasks.new(get("tasks", opts), opts, client: self)
47
+ end
48
+
49
+
50
+ def users(opts = {})
51
+ Harvesting::Models::Users.new(get("users", opts), opts, client: self)
52
+ end
53
+
54
+ def invoices
55
+ get("invoices")["invoices"].map do |result|
56
+ Harvesting::Models::Invoice.new(result, client: self)
57
+ end
50
58
  end
51
59
 
52
60
  def create(entity)
@@ -65,7 +73,12 @@ module Harvesting
65
73
  entity
66
74
  end
67
75
 
68
- private
76
+ def delete(entity)
77
+ url = "#{DEFAULT_HOST}/#{entity.path}"
78
+ uri = URI(url)
79
+ response = http_response(:delete, uri)
80
+ raise UnprocessableRequest(response.to_s) unless response.code.to_i == 200
81
+ end
69
82
 
70
83
  def get(path, opts = {})
71
84
  url = "#{DEFAULT_HOST}/#{path}"
@@ -75,10 +88,12 @@ module Harvesting
75
88
  JSON.parse(response.body)
76
89
  end
77
90
 
91
+ private
92
+
78
93
  def http_response(method, uri, opts = {})
79
94
  response = nil
80
95
 
81
- http = HTTP["User-Agent" => "Ruby Harvest API Sample",
96
+ http = HTTP["User-Agent" => "Harvesting Ruby Gem",
82
97
  "Authorization" => "Bearer #{@access_token}",
83
98
  "Harvest-Account-ID" => @account_id]
84
99
  params = {}
@@ -2,36 +2,64 @@ module Harvesting
2
2
  module Models
3
3
  class Base
4
4
  attr_accessor :attributes
5
- attr_reader :client
5
+ attr_reader :harvest_client
6
6
 
7
7
  def initialize(attrs, opts = {})
8
+ @models = {}
8
9
  @attributes = attrs.dup
9
- @client = opts[:client] || Harvesting::Client.new(opts)
10
+ @harvest_client = opts[:client] || Harvesting::Client.new(opts)
10
11
  end
11
12
 
12
13
  def self.attributed(*attribute_names)
13
14
  attribute_names.each do |attribute_name|
14
- Harvesting::Models::Base.send :define_method, attribute_name.to_s do
15
+ define_method(attribute_name) do
15
16
  @attributes[__method__.to_s]
16
17
  end
17
18
  end
18
19
  end
19
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
28
+ end
29
+
20
30
  def save
21
31
  id.nil? ? create : update
22
32
  end
23
33
 
24
34
  def create
25
- @client.create(self)
35
+ @harvest_client.create(self)
26
36
  end
27
37
 
28
38
  def update
29
- @client.update(self)
39
+ @harvest_client.update(self)
40
+ end
41
+
42
+ def delete
43
+ @harvest_client.delete(self)
30
44
  end
31
45
 
32
46
  def to_hash
33
47
  @attributes
34
48
  end
49
+
50
+ def fetch
51
+ self.class.new(@harvest_client.get(path), client: @harvest_client)
52
+ end
53
+
54
+ # Retrieves an instance of the object by ID
55
+ #
56
+ # @param id [Integer] the id of the object to retrieve
57
+ # @param opts [Hash] options to pass along to the `Harvesting::Client`
58
+ # instance
59
+ def self.get(id, opts = {})
60
+ client = opts[:client] || Harvesting::Client.new(opts)
61
+ self.new({ 'id' => id }, opts).fetch
62
+ end
35
63
  end
36
64
  end
37
65
  end
@@ -1,6 +1,6 @@
1
1
  module Harvesting
2
2
  module Models
3
- class Client < Base
3
+ class Client < HarvestRecord
4
4
  attributed :id,
5
5
  :name,
6
6
  :is_active,
@@ -9,6 +9,9 @@ module Harvesting
9
9
  :updated_at,
10
10
  :currency
11
11
 
12
+ def path
13
+ @attributes['id'].nil? ? "clients" : "clients/#{@attributes['id']}"
14
+ end
12
15
  end
13
16
  end
14
17
  end
@@ -1,6 +1,6 @@
1
1
  module Harvesting
2
2
  module Models
3
- class Contact < Base
3
+ class Contact < HarvestRecord
4
4
  attributed :id,
5
5
  :title,
6
6
  :first_name,
@@ -11,6 +11,16 @@ module Harvesting
11
11
  :fax,
12
12
  :created_at,
13
13
  :updated_at
14
+
15
+ modeled client: Client
16
+
17
+ def path
18
+ @attributes['id'].nil? ? "contacts" : "contacts/#{@attributes['id']}"
19
+ end
20
+
21
+ def to_hash
22
+ { client_id: client.id }.merge(super)
23
+ end
14
24
  end
15
25
  end
16
26
  end
@@ -0,0 +1,18 @@
1
+ module Harvesting
2
+ module Models
3
+ class HarvestRecord < Base
4
+
5
+ def save
6
+ id.nil? ? create : update
7
+ end
8
+
9
+ def create
10
+ harvest_client.create(self)
11
+ end
12
+
13
+ def update
14
+ harvest_client.update(self)
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,41 @@
1
+ module Harvesting
2
+ module Models
3
+ class HarvestRecordCollection < Base
4
+ include Harvesting::Enumerable
5
+ extend Forwardable
6
+
7
+ attributed :per_page,
8
+ :total_pages,
9
+ :total_entries,
10
+ :next_page,
11
+ :previous_page,
12
+ :page,
13
+ :links
14
+
15
+ attr_reader :entries
16
+
17
+ def initialize(attrs, query_opts = {}, opts = {})
18
+ super(attrs, opts)
19
+ @query_opts = query_opts
20
+ @api_page = attrs
21
+ end
22
+
23
+ def page
24
+ @attributes['page']
25
+ end
26
+
27
+ def size
28
+ total_entries
29
+ end
30
+
31
+ def next_page_query_opts
32
+ @query_opts.merge(page: page + 1)
33
+ end
34
+
35
+ def fetch_next_page
36
+ raise NotImplementedError
37
+ end
38
+
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,37 @@
1
+ module Harvesting
2
+ module Models
3
+ class Invoice < HarvestRecord
4
+ attributed :id,
5
+ :client_key,
6
+ :line_items,
7
+ :number,
8
+ :purchase_order,
9
+ :amount,
10
+ :due_amount,
11
+ :tax,
12
+ :tax_amount,
13
+ :tax2,
14
+ :tax2_amount,
15
+ :discount,
16
+ :discount_amount,
17
+ :subject,
18
+ :notes,
19
+ :currency,
20
+ :state,
21
+ :period_start,
22
+ :period_end,
23
+ :issue_date,
24
+ :due_date,
25
+ :payment_term,
26
+ :sent_at,
27
+ :paid_at,
28
+ :paid_date,
29
+ :closed_at,
30
+ :created_at,
31
+ :updated_at
32
+ def path
33
+ @attributes['id'].nil? ? "invoices" : "invoices/#{@attributes['id']}"
34
+ end
35
+ end
36
+ end
37
+ end
@@ -1,8 +1,7 @@
1
1
  module Harvesting
2
2
  module Models
3
- class Project < Base
3
+ class Project < HarvestRecord
4
4
  attributed :id,
5
- :client,
6
5
  :name,
7
6
  :code,
8
7
  :is_active,
@@ -12,6 +11,7 @@ module Harvesting
12
11
  :hourly_rate,
13
12
  :budget,
14
13
  :budget_by,
14
+ :budget_is_monthly,
15
15
  :notify_when_over_budget,
16
16
  :over_budget_notification_percentage,
17
17
  :over_budget_notification_date,
@@ -25,8 +25,14 @@ module Harvesting
25
25
  :created_at,
26
26
  :updated_at
27
27
 
28
+ modeled client: Client
29
+
28
30
  def path
29
- id.nil? ? "projects" : "projects/#{id}"
31
+ @attributes['id'].nil? ? "projects" : "projects/#{@attributes['id']}"
32
+ end
33
+
34
+ def to_hash
35
+ { client_id: client.id }.merge(super)
30
36
  end
31
37
  end
32
38
  end
@@ -1,44 +1,17 @@
1
1
  module Harvesting
2
2
  module Models
3
- class Projects < Base
4
- include Harvesting::Enumerable
5
- extend Forwardable
3
+ class Projects < HarvestRecordCollection
6
4
 
7
- attributed :per_page,
8
- :total_pages,
9
- :total_entries,
10
- :next_page,
11
- :previous_page,
12
- :page,
13
- :links
14
-
15
- attr_reader :entries
16
-
17
- def initialize(attrs, opts = {})
18
- super(attrs.reject {|k,v| k == "projects" }, opts)
19
- @api_page = attrs
5
+ def initialize(attrs, query_opts = {}, opts = {})
6
+ super(attrs.reject {|k,v| k == "projects" }, query_opts, opts)
20
7
  @entries = attrs["projects"].map do |entry|
21
8
  Project.new(entry, client: opts[:client])
22
9
  end
23
10
  end
24
11
 
25
- # def each
26
- # @entries.each_with_index do |time_entry, index|
27
- # yield(time_entry)
28
- # end
29
- # end
30
- def page
31
- @attributes['page']
32
- end
33
-
34
- def size
35
- total_entries
36
- end
37
-
38
12
  def fetch_next_page
39
- new_page = page + 1
40
- @entries += client.projects(page: new_page).entries
41
- @attributes['page'] = new_page
13
+ @entries += harvest_client.projects(next_page_query_opts).entries
14
+ @attributes['page'] = page + 1
42
15
  end
43
16
  end
44
17
  end
@@ -1,6 +1,6 @@
1
1
  module Harvesting
2
2
  module Models
3
- class Task < Base
3
+ class Task < HarvestRecord
4
4
  attributed :id,
5
5
  :name,
6
6
  :billable_by_default,
@@ -11,7 +11,7 @@ module Harvesting
11
11
  :updated_at
12
12
 
13
13
  def path
14
- id.nil? ? "tasks" : "tasks/#{id}"
14
+ @attributes['id'].nil? ? "tasks" : "tasks/#{@attributes['id']}"
15
15
  end
16
16
  end
17
17
  end
@@ -0,0 +1,31 @@
1
+ module Harvesting
2
+ module Models
3
+ class TaskAssignment < HarvestRecord
4
+ attributed :id,
5
+ :is_active,
6
+ :billable,
7
+ :hourly_rate,
8
+ :budget,
9
+ :created_at,
10
+ :updated_at
11
+
12
+ modeled project: Project,
13
+ task: Task
14
+
15
+ def path
16
+ base_url = "projects/#{project.id}/task_assignments"
17
+ id.nil? ? base_url : "#{base_url}/#{id}"
18
+ end
19
+
20
+ # def project_id
21
+ # # TODO: handle case where project's id is part of json object
22
+ # @attributes["project_id"]
23
+ # end
24
+
25
+ def to_hash
26
+ { project_id: project.id, task_id: task.id }.merge(super)
27
+ end
28
+
29
+ end
30
+ end
31
+ end
@@ -1,44 +1,17 @@
1
1
  module Harvesting
2
2
  module Models
3
- class Tasks < Base
4
- include Harvesting::Enumerable
5
- extend Forwardable
3
+ class Tasks < HarvestRecordCollection
6
4
 
7
- attributed :per_page,
8
- :total_pages,
9
- :total_entries,
10
- :next_page,
11
- :previous_page,
12
- :page,
13
- :links
14
-
15
- attr_reader :entries
16
-
17
- def initialize(attrs, opts = {})
18
- super(attrs.reject {|k,v| k == "tasks" }, opts)
19
- @api_page = attrs
5
+ def initialize(attrs, query_opts = {}, opts = {})
6
+ super(attrs.reject {|k,v| k == "tasks" }, query_opts, opts)
20
7
  @entries = attrs["tasks"].map do |entry|
21
8
  Task.new(entry, client: opts[:client])
22
9
  end
23
10
  end
24
11
 
25
- # def each
26
- # @entries.each_with_index do |time_entry, index|
27
- # yield(time_entry)
28
- # end
29
- # end
30
- def page
31
- @attributes['page']
32
- end
33
-
34
- def size
35
- total_entries
36
- end
37
-
38
12
  def fetch_next_page
39
- new_page = page + 1
40
- @entries += client.tasks(page: new_page).entries
41
- @attributes['page'] = new_page
13
+ @entries += harvest_client.tasks(next_page_query_opts).entries
14
+ @attributes['page'] = page + 1
42
15
  end
43
16
  end
44
17
  end
@@ -1,44 +1,17 @@
1
1
  module Harvesting
2
2
  module Models
3
- class TimeEntries < Base
4
- include Harvesting::Enumerable
5
- extend Forwardable
3
+ class TimeEntries < HarvestRecordCollection
6
4
 
7
- attributed :per_page,
8
- :total_pages,
9
- :total_entries,
10
- :next_page,
11
- :previous_page,
12
- :page,
13
- :links
14
-
15
- attr_reader :entries
16
-
17
- def initialize(attrs, opts = {})
18
- super(attrs.reject {|k,v| k == "time_entries" }, opts)
19
- @api_page = attrs
5
+ def initialize(attrs, query_opts = {}, opts = {})
6
+ super(attrs.reject {|k,v| k == "time_entries" }, query_opts, opts)
20
7
  @entries = attrs["time_entries"].map do |entry|
21
8
  TimeEntry.new(entry, client: opts[:client])
22
9
  end
23
10
  end
24
11
 
25
- # def each
26
- # @entries.each_with_index do |time_entry, index|
27
- # yield(time_entry)
28
- # end
29
- # end
30
- def page
31
- @attributes['page']
32
- end
33
-
34
- def size
35
- total_entries
36
- end
37
-
38
12
  def fetch_next_page
39
- new_page = page + 1
40
- @entries += @client.time_entries(page: new_page).entries
41
- @attributes['page'] = new_page
13
+ @entries += harvest_client.time_entries(next_page_query_opts).entries
14
+ @attributes['page'] = page + 1
42
15
  end
43
16
  end
44
17
  end
@@ -1,12 +1,10 @@
1
1
  module Harvesting
2
2
  module Models
3
- class TimeEntry < Base
3
+ class TimeEntry < HarvestRecord
4
4
  attributed :id,
5
5
  :spent_date,
6
6
  :hours,
7
7
  :notes,
8
- :created_at,
9
- :updated_at,
10
8
  :is_locked,
11
9
  :locked_reason,
12
10
  :is_closed,
@@ -15,22 +13,31 @@ module Harvesting
15
13
  :started_time,
16
14
  :ended_time,
17
15
  :is_running,
18
- :invoice,
19
- :external_reference,
20
16
  :billable,
21
17
  :budgeted,
22
18
  :billable_rate,
23
19
  :cost_rate,
24
- :project_id,
25
- :task_id
20
+ :invoice,
21
+ :external_reference,
22
+ :created_at,
23
+ :updated_at,
24
+ :user_assignment # temporarily return the hash itself until the model is added
25
+
26
+ modeled project: Project,
27
+ user: User,
28
+ task: Task,
29
+ client: Client,
30
+ task_assignment: TaskAssignment
31
+
26
32
 
27
33
  def path
28
- id.nil? ? "time_entries" : "time_entries/#{id}"
34
+ @attributes['id'].nil? ? "time_entries" : "time_entries/#{@attributes['id']}"
29
35
  end
30
36
 
31
- def user
32
- Models::User.new(@attributes['user'], client: @client)
37
+ def to_hash
38
+ { project_id: project.id, task_id: task.id, user_id: user.id }.merge(super)
33
39
  end
40
+
34
41
  end
35
42
  end
36
43
  end
@@ -1,25 +1,31 @@
1
1
  module Harvesting
2
2
  module Models
3
- class User < Base
3
+ class User < HarvestRecord
4
4
  attributed :id,
5
5
  :first_name,
6
6
  :last_name,
7
7
  :email,
8
8
  :telephone,
9
9
  :timezone,
10
- :weekly_capacity,
11
10
  :has_access_to_all_future_projects,
12
11
  :is_contractor,
13
12
  :is_admin,
14
13
  :is_project_manager,
15
14
  :can_see_rates,
16
- :can_create_projects,
17
15
  :can_create_invoices,
16
+ :can_create_projects,
18
17
  :is_active,
19
- :created_at,
20
- :updated_at,
18
+ :weekly_capacity,
19
+ :default_hourly_rate,
20
+ :cost_rate,
21
21
  :roles,
22
- :avatar_url
22
+ :avatar_url,
23
+ :created_at,
24
+ :updated_at
25
+
26
+ def path
27
+ @attributes['id'].nil? ? "users" : "users/#{@attributes['id']}"
28
+ end
23
29
  end
24
30
  end
25
31
  end
@@ -0,0 +1,18 @@
1
+ module Harvesting
2
+ module Models
3
+ class Users < HarvestRecordCollection
4
+
5
+ def initialize(attrs, query_opts = {}, opts = {})
6
+ super(attrs.reject {|k,v| k == "users" }, query_opts, opts)
7
+ @entries = attrs["users"].map do |entry|
8
+ User.new(entry, client: opts[:client])
9
+ end
10
+ end
11
+
12
+ def fetch_next_page
13
+ @entries += harvest_client.users(next_page_query_opts).entries
14
+ @attributes['page'] = page + 1
15
+ end
16
+ end
17
+ end
18
+ end
@@ -1,3 +1,3 @@
1
1
  module Harvesting
2
- VERSION = "0.2.0".freeze
2
+ VERSION = "0.3.0".freeze
3
3
  end
metadata CHANGED
@@ -1,33 +1,33 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: harvesting
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ernesto Tagwerker
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2018-10-18 00:00:00.000000000 Z
11
+ date: 2019-01-22 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: http
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
- - - "~>"
17
+ - - ">="
18
18
  - !ruby/object:Gem::Version
19
19
  version: '3.3'
20
- - - ">="
20
+ - - "~>"
21
21
  - !ruby/object:Gem::Version
22
22
  version: '3.3'
23
23
  type: :runtime
24
24
  prerelease: false
25
25
  version_requirements: !ruby/object:Gem::Requirement
26
26
  requirements:
27
- - - "~>"
27
+ - - ">="
28
28
  - !ruby/object:Gem::Version
29
29
  version: '3.3'
30
- - - ">="
30
+ - - "~>"
31
31
  - !ruby/object:Gem::Version
32
32
  version: '3.3'
33
33
  - !ruby/object:Gem::Dependency
@@ -76,82 +76,102 @@ dependencies:
76
76
  name: guard-rspec
77
77
  requirement: !ruby/object:Gem::Requirement
78
78
  requirements:
79
- - - "~>"
79
+ - - ">="
80
80
  - !ruby/object:Gem::Version
81
81
  version: '4.7'
82
- - - ">="
82
+ - - "~>"
83
83
  - !ruby/object:Gem::Version
84
84
  version: '4.7'
85
85
  type: :development
86
86
  prerelease: false
87
87
  version_requirements: !ruby/object:Gem::Requirement
88
88
  requirements:
89
- - - "~>"
89
+ - - ">="
90
90
  - !ruby/object:Gem::Version
91
91
  version: '4.7'
92
- - - ">="
92
+ - - "~>"
93
93
  - !ruby/object:Gem::Version
94
94
  version: '4.7'
95
95
  - !ruby/object:Gem::Dependency
96
96
  name: byebug
97
97
  requirement: !ruby/object:Gem::Requirement
98
98
  requirements:
99
- - - "~>"
99
+ - - ">="
100
100
  - !ruby/object:Gem::Version
101
101
  version: '10.0'
102
- - - ">="
102
+ - - "~>"
103
103
  - !ruby/object:Gem::Version
104
104
  version: '10.0'
105
105
  type: :development
106
106
  prerelease: false
107
107
  version_requirements: !ruby/object:Gem::Requirement
108
108
  requirements:
109
- - - "~>"
109
+ - - ">="
110
110
  - !ruby/object:Gem::Version
111
111
  version: '10.0'
112
- - - ">="
112
+ - - "~>"
113
113
  - !ruby/object:Gem::Version
114
114
  version: '10.0'
115
115
  - !ruby/object:Gem::Dependency
116
116
  name: vcr
117
117
  requirement: !ruby/object:Gem::Requirement
118
118
  requirements:
119
- - - "~>"
119
+ - - ">="
120
120
  - !ruby/object:Gem::Version
121
121
  version: '4.0'
122
- - - ">="
122
+ - - "~>"
123
123
  - !ruby/object:Gem::Version
124
124
  version: '4.0'
125
125
  type: :development
126
126
  prerelease: false
127
127
  version_requirements: !ruby/object:Gem::Requirement
128
128
  requirements:
129
- - - "~>"
129
+ - - ">="
130
130
  - !ruby/object:Gem::Version
131
131
  version: '4.0'
132
- - - ">="
132
+ - - "~>"
133
133
  - !ruby/object:Gem::Version
134
134
  version: '4.0'
135
135
  - !ruby/object:Gem::Dependency
136
136
  name: webmock
137
137
  requirement: !ruby/object:Gem::Requirement
138
138
  requirements:
139
- - - "~>"
139
+ - - ">="
140
140
  - !ruby/object:Gem::Version
141
141
  version: '3.4'
142
- - - ">="
142
+ - - "~>"
143
143
  - !ruby/object:Gem::Version
144
144
  version: '3.4'
145
145
  type: :development
146
146
  prerelease: false
147
147
  version_requirements: !ruby/object:Gem::Requirement
148
148
  requirements:
149
+ - - ">="
150
+ - !ruby/object:Gem::Version
151
+ version: '3.4'
149
152
  - - "~>"
150
153
  - !ruby/object:Gem::Version
151
154
  version: '3.4'
155
+ - !ruby/object:Gem::Dependency
156
+ name: dotenv
157
+ requirement: !ruby/object:Gem::Requirement
158
+ requirements:
152
159
  - - ">="
153
160
  - !ruby/object:Gem::Version
154
- version: '3.4'
161
+ version: '2.5'
162
+ - - "~>"
163
+ - !ruby/object:Gem::Version
164
+ version: '2.5'
165
+ type: :development
166
+ prerelease: false
167
+ version_requirements: !ruby/object:Gem::Requirement
168
+ requirements:
169
+ - - ">="
170
+ - !ruby/object:Gem::Version
171
+ version: '2.5'
172
+ - - "~>"
173
+ - !ruby/object:Gem::Version
174
+ version: '2.5'
155
175
  description: Interact with the Harvest API v2.0 from your Ruby application
156
176
  email:
157
177
  - ernesto+github@ombulabs.com
@@ -159,6 +179,7 @@ executables: []
159
179
  extensions: []
160
180
  extra_rdoc_files: []
161
181
  files:
182
+ - ".env.sample"
162
183
  - ".gitignore"
163
184
  - ".travis.yml"
164
185
  - CODE_OF_CONDUCT.md
@@ -182,13 +203,18 @@ files:
182
203
  - lib/harvesting/models/base.rb
183
204
  - lib/harvesting/models/client.rb
184
205
  - lib/harvesting/models/contact.rb
206
+ - lib/harvesting/models/harvest_record.rb
207
+ - lib/harvesting/models/harvest_record_collection.rb
208
+ - lib/harvesting/models/invoice.rb
185
209
  - lib/harvesting/models/project.rb
186
210
  - lib/harvesting/models/projects.rb
187
211
  - lib/harvesting/models/task.rb
212
+ - lib/harvesting/models/task_assignment.rb
188
213
  - lib/harvesting/models/tasks.rb
189
214
  - lib/harvesting/models/time_entries.rb
190
215
  - lib/harvesting/models/time_entry.rb
191
216
  - lib/harvesting/models/user.rb
217
+ - lib/harvesting/models/users.rb
192
218
  - lib/harvesting/version.rb
193
219
  homepage: https://github.com/ombulabs/harvesting
194
220
  licenses:
@@ -210,7 +236,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
210
236
  version: '0'
211
237
  requirements: []
212
238
  rubyforge_project:
213
- rubygems_version: 2.7.6
239
+ rubygems_version: 2.7.8
214
240
  signing_key:
215
241
  specification_version: 4
216
242
  summary: Ruby wrapper for the Harvest API v2.0