forecasted 0.0.1

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.
Files changed (79) hide show
  1. checksums.yaml +7 -0
  2. data/.document +3 -0
  3. data/.gitignore +39 -0
  4. data/.rspec +2 -0
  5. data/.ruby-version +1 -0
  6. data/.travis.yml +16 -0
  7. data/Gemfile +19 -0
  8. data/HISTORY.md +19 -0
  9. data/MIT-LICENSE +20 -0
  10. data/README.md +109 -0
  11. data/Rakefile +22 -0
  12. data/examples/basics.rb +35 -0
  13. data/examples/clear_account.rb +28 -0
  14. data/examples/project_create_script.rb +93 -0
  15. data/examples/task_assignments.rb +27 -0
  16. data/examples/user_assignments.rb +24 -0
  17. data/forecasted.gemspec +25 -0
  18. data/lib/ext/array.rb +52 -0
  19. data/lib/ext/date.rb +9 -0
  20. data/lib/ext/hash.rb +17 -0
  21. data/lib/ext/time.rb +5 -0
  22. data/lib/forecast/aggregate.rb +8 -0
  23. data/lib/forecast/api/account.rb +22 -0
  24. data/lib/forecast/api/aggregates.rb +20 -0
  25. data/lib/forecast/api/assignments.rb +63 -0
  26. data/lib/forecast/api/base.rb +68 -0
  27. data/lib/forecast/api/clients.rb +10 -0
  28. data/lib/forecast/api/expense_categories.rb +9 -0
  29. data/lib/forecast/api/expenses.rb +27 -0
  30. data/lib/forecast/api/invoice_categories.rb +26 -0
  31. data/lib/forecast/api/invoice_messages.rb +75 -0
  32. data/lib/forecast/api/invoice_payments.rb +31 -0
  33. data/lib/forecast/api/invoices.rb +35 -0
  34. data/lib/forecast/api/milestones.rb +21 -0
  35. data/lib/forecast/api/projects.rb +23 -0
  36. data/lib/forecast/api/reports.rb +53 -0
  37. data/lib/forecast/api/tasks.rb +36 -0
  38. data/lib/forecast/api/time.rb +48 -0
  39. data/lib/forecast/api/user_assignments.rb +34 -0
  40. data/lib/forecast/api/users.rb +21 -0
  41. data/lib/forecast/assignment.rb +7 -0
  42. data/lib/forecast/base.rb +111 -0
  43. data/lib/forecast/behavior/activatable.rb +31 -0
  44. data/lib/forecast/behavior/crud.rb +75 -0
  45. data/lib/forecast/client.rb +22 -0
  46. data/lib/forecast/credentials.rb +42 -0
  47. data/lib/forecast/errors.rb +26 -0
  48. data/lib/forecast/expense.rb +27 -0
  49. data/lib/forecast/expense_category.rb +10 -0
  50. data/lib/forecast/hardy_client.rb +80 -0
  51. data/lib/forecast/invoice.rb +107 -0
  52. data/lib/forecast/invoice_category.rb +9 -0
  53. data/lib/forecast/invoice_message.rb +8 -0
  54. data/lib/forecast/invoice_payment.rb +8 -0
  55. data/lib/forecast/line_item.rb +4 -0
  56. data/lib/forecast/model.rb +154 -0
  57. data/lib/forecast/project.rb +37 -0
  58. data/lib/forecast/rate_limit_status.rb +23 -0
  59. data/lib/forecast/task.rb +22 -0
  60. data/lib/forecast/time_entry.rb +25 -0
  61. data/lib/forecast/timezones.rb +130 -0
  62. data/lib/forecast/trackable_project.rb +38 -0
  63. data/lib/forecast/user_assignment.rb +30 -0
  64. data/lib/forecast/version.rb +3 -0
  65. data/lib/forecasted.rb +87 -0
  66. data/spec/factories.rb +17 -0
  67. data/spec/forecast/base_spec.rb +11 -0
  68. data/spec/functional/aggregates_spec.rb +64 -0
  69. data/spec/functional/assignments_spec.rb +131 -0
  70. data/spec/functional/errors_spec.rb +22 -0
  71. data/spec/functional/hardy_client_spec.rb +33 -0
  72. data/spec/functional/milestones_spec.rb +82 -0
  73. data/spec/functional/people_spec.rb +85 -0
  74. data/spec/functional/project_spec.rb +41 -0
  75. data/spec/spec_helper.rb +41 -0
  76. data/spec/support/forecast_credentials.example.yml +6 -0
  77. data/spec/support/forecasted_helpers.rb +66 -0
  78. data/spec/support/json_examples.rb +9 -0
  79. metadata +189 -0
@@ -0,0 +1,63 @@
1
+ require 'business_time'
2
+
3
+ module Forecast
4
+
5
+ FORECAST_DATE_FORMAT = "%Y-%m-%d"
6
+
7
+ module API
8
+ class Assignments < Base
9
+ api_model Forecast::Assignment
10
+ include Forecast::Behavior::Crud
11
+
12
+ def by_project(project_id, query_options={})
13
+ query = {project_id: project_id}.merge(query_options)
14
+ self.all(query)
15
+ end
16
+
17
+ def sum_allocation_seconds(query)
18
+ axs = self.all(query)
19
+
20
+ total_time = 0
21
+
22
+ axs.each do |x|
23
+ start_date = Date.strptime(x.start_date, FORECAST_DATE_FORMAT)
24
+ end_date = Date.strptime(x.end_date, FORECAST_DATE_FORMAT)
25
+
26
+ num_days = start_date.business_days_until(end_date) + 1
27
+
28
+ total_time += (1.0 * x.allocation * num_days)
29
+ end
30
+
31
+ return total_time
32
+ end
33
+
34
+ def last_by_project(project_id)
35
+ x = self.by_project(project_id)
36
+ self.last_by_date(x)
37
+ end
38
+
39
+
40
+ def create(*) ; raise "not implemented" ; end
41
+ def update(*) ; raise "not implemented" ; end
42
+ def delete(*) ; raise "not implemented" ; end
43
+
44
+
45
+ def last_by_date(array_hm)
46
+ array_hm.sort_by do |x|
47
+ Date.strptime(x.end_date, FORECAST_DATE_FORMAT)
48
+ end.last
49
+ end
50
+
51
+ def business_days_between(date1, date2)
52
+ business_days = 0
53
+ date = date2
54
+ while date > date1
55
+ business_days = business_days + 1 unless date.saturday? or date.sunday?
56
+ date = date - 1.day
57
+ end
58
+ business_days
59
+ end
60
+
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,68 @@
1
+ module Forecast
2
+ module API
3
+ class Base
4
+ attr_reader :credentials
5
+
6
+ def initialize(credentials)
7
+ @credentials = credentials
8
+ end
9
+
10
+ class << self
11
+ def api_model(klass)
12
+ class_eval <<-END
13
+ def api_model
14
+ #{klass}
15
+ end
16
+ END
17
+ end
18
+ end
19
+
20
+ protected
21
+ def request(method, credentials, path, options = {})
22
+ params = {
23
+ path: path,
24
+ options: options,
25
+ method: method
26
+ }
27
+
28
+ httparty_options = {
29
+ query: options[:query],
30
+ body: options[:body],
31
+ format: :plain,
32
+ headers: {
33
+ "Accept" => "application/json",
34
+ "Content-Type" => "application/json; charset=utf-8",
35
+ "User-Agent" => "Forecasted/#{Forecast::VERSION}"
36
+ }.update(options[:headers] || {})
37
+ }
38
+
39
+ credentials.set_authentication(httparty_options)
40
+ response = HTTParty.send(method, "#{credentials.host}#{path}", httparty_options)
41
+ params[:response] = response.inspect.to_s
42
+
43
+ case response.code
44
+ when 200..201
45
+ response
46
+ when 400
47
+ raise Forecast::BadRequest.new(response, params)
48
+ when 401
49
+ raise Forecast::AuthenticationFailed.new(response, params)
50
+ when 404
51
+ raise Forecast::NotFound.new(response, params, "Do you have sufficient privileges?")
52
+ when 500
53
+ raise Forecast::ServerError.new(response, params)
54
+ when 502
55
+ raise Forecast::Unavailable.new(response, params)
56
+ when 503
57
+ raise Forecast::RateLimited.new(response, params)
58
+ else
59
+ raise Forecast::InformHarvest.new(response, params)
60
+ end
61
+ end
62
+
63
+ def of_user_query(user)
64
+ query = user.nil? ? {} : {"of_user" => user.to_i}
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,10 @@
1
+ module Harvest
2
+ module API
3
+ class Clients < Base
4
+ api_model Harvest::Client
5
+
6
+ include Harvest::Behavior::Crud
7
+ include Harvest::Behavior::Activatable
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,9 @@
1
+ module Harvest
2
+ module API
3
+ class ExpenseCategories < Base
4
+ api_model Harvest::ExpenseCategory
5
+
6
+ include Harvest::Behavior::Crud
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,27 @@
1
+ module Harvest
2
+ module API
3
+ class Expenses < Base
4
+ api_model Harvest::Expense
5
+
6
+ include Harvest::Behavior::Crud
7
+
8
+ def all(date = ::Time.now, user = nil)
9
+ date = ::Time.parse(date) if String === date
10
+ response = request(:get, credentials, "#{api_model.api_path}/#{date.yday}/#{date.year}", :query => of_user_query(user))
11
+ api_model.parse(response.parsed_response)
12
+ end
13
+
14
+ # This is currently broken, but will come back to it
15
+ def attach(expense, filename, receipt)
16
+ body = ""
17
+ body << "------------------------------b7edea381b46\r\n"
18
+ body << %Q{Content-Disposition: form-data; name="expense[receipt]"; filename="#{filename}"\r\n}
19
+ body << "Content-Type: image/png\r\n"
20
+ body << "\r\n#{receipt.read}\r\n"
21
+ body << "------------------------------b7edea381b46\r\n"
22
+
23
+ request(:post, credentials, "#{api_model.api_path}/#{expense.to_i}/receipt", :headers => {'Content-Type' => 'multipart/form-data; boundary=------------------------------b7edea381b46'}, :body => body)
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,26 @@
1
+ module Harvest
2
+ module API
3
+ class InvoiceCategories < Base
4
+ api_model Harvest::InvoiceCategory
5
+ include Harvest::Behavior::Crud
6
+
7
+ def find(*)
8
+ raise "find is unsupported for InvoiceCategories"
9
+ end
10
+
11
+ def create(model)
12
+ model = api_model.wrap(model)
13
+ response = request(:post, credentials, "#{api_model.api_path}", :body => model.to_json)
14
+ id = response.headers["location"].match(/\/.*\/(\d+)/)[1]
15
+ all.detect {|c| c.id == id.to_i }
16
+ end
17
+
18
+ def update(model, user = nil)
19
+ model = api_model.wrap(model)
20
+ request(:put, credentials, "#{api_model.api_path}/#{model.to_i}", :body => model.to_json, :query => of_user_query(user))
21
+ all.detect {|c| c.id == model.id }
22
+ end
23
+
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,75 @@
1
+ module Harvest
2
+ module API
3
+ class InvoiceMessages < Base
4
+ api_model Harvest::InvoiceMessage
5
+ include Harvest::Behavior::Crud
6
+
7
+ def all(invoice)
8
+ response = request(:get, credentials, "/invoices/#{invoice.to_i}/messages")
9
+ api_model.parse(response.parsed_response)
10
+ end
11
+
12
+ def find(invoice, message)
13
+ response = request(:get, credentials, "/invoices/#{invoice.to_i}/messages/#{message.to_i}")
14
+ api_model.parse(response.parsed_response).first
15
+ end
16
+
17
+ def create(message)
18
+ message = api_model.wrap(message)
19
+ response = request(:post, credentials, "/invoices/#{message.invoice_id}/messages", :body => message.to_json)
20
+ id = response.headers["location"].match(/\/.*\/(\d+)\/.*\/(\d+)/)[2]
21
+ find(message.invoice_id, id)
22
+ end
23
+
24
+ def delete(message)
25
+ request(:delete, credentials, "/invoices/#{message.invoice_id}/messages/#{message.to_i}")
26
+ message.id
27
+ end
28
+
29
+ # Create a message for marking an invoice as sent.
30
+ #
31
+ # @param [Harvest::InvoiceMessage] The message you want to send
32
+ # @return [Harvest::InvoiceMessage] The sent message
33
+ def mark_as_sent(message)
34
+ send_status_message(message, 'mark_as_sent')
35
+ end
36
+
37
+ # Create a message and mark an open invoice as closed (writing an invoice off)
38
+ #
39
+ # @param [Harvest::InvoiceMessage] The message you want to send
40
+ # @return [Harvest::InvoiceMessage] The sent message
41
+ def mark_as_closed(message)
42
+ send_status_message(message, 'mark_as_closed')
43
+ end
44
+
45
+ # Create a message and mark a closed (written-off) invoice as open
46
+ #
47
+ # @param [Harvest::InvoiceMessage] The message you want to send
48
+ # @return [Harvest::InvoiceMessage] The sent message
49
+ def re_open(message)
50
+ send_status_message(message, 're_open')
51
+ end
52
+
53
+ # Create a message for marking an open invoice as draft
54
+ #
55
+ # @param [Harvest::InvoiceMessage] The message you want to send
56
+ # @return [Harvest::InvoiceMessage] The sent message
57
+ def mark_as_draft(message)
58
+ send_status_message(message, 'mark_as_draft')
59
+ end
60
+
61
+ private
62
+
63
+ def send_status_message(message, action)
64
+ message = api_model.wrap(message)
65
+ response = request( :post,
66
+ credentials,
67
+ "/invoices/#{message.invoice_id}/messages/#{action}",
68
+ :body => message.to_json
69
+ )
70
+ message
71
+ end
72
+
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,31 @@
1
+ module Harvest
2
+ module API
3
+ class InvoicePayments < Base
4
+ api_model Harvest::InvoicePayment
5
+ include Harvest::Behavior::Crud
6
+
7
+ def all(invoice)
8
+ response = request(:get, credentials, "/invoices/#{invoice.to_i}/payments")
9
+ api_model.parse(response.parsed_response)
10
+ end
11
+
12
+ def find(invoice, payment)
13
+ response = request(:get, credentials, "/invoices/#{invoice.to_i}/payments/#{payment.to_i}")
14
+ api_model.parse(response.parsed_response).first
15
+ end
16
+
17
+ def create(payment)
18
+ payment = api_model.wrap(payment)
19
+ response = request(:post, credentials, "/invoices/#{payment.invoice_id}/payments", :body => payment.to_json)
20
+ id = response.headers["location"].match(/\/.*\/(\d+)\/.*\/(\d+)/)[2]
21
+ find(payment.invoice_id, id)
22
+ end
23
+
24
+ def delete(payment)
25
+ request(:delete, credentials, "/invoices/#{payment.invoice_id}/payments/#{payment.to_i}")
26
+ payment.id
27
+ end
28
+
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,35 @@
1
+ module Harvest
2
+ module API
3
+ class Invoices < Base
4
+ api_model Harvest::Invoice
5
+ include Harvest::Behavior::Crud
6
+
7
+ # == Retrieves invoices
8
+ #
9
+ # == Available options
10
+ # - :status - invoices by status
11
+ # - :page
12
+ # - :updated_since
13
+ # - :timeframe (must be a nested hash with :to and :from)
14
+ #
15
+ # @overload all()
16
+ # @overload all(options)
17
+ # @param [Hash] filtering options
18
+ #
19
+ # @return [Array<Harvest::Invoice>] an array of invoices
20
+ def all(options = {})
21
+ query = {}
22
+ query[:status] = options[:status] if options[:status]
23
+ query[:page] = options[:page] if options[:page]
24
+ query[:updated_since] = options[:updated_since] if options[:updated_since]
25
+ if options[:timeframe]
26
+ query[:from] = options[:timeframe][:from]
27
+ query[:to] = options[:timeframe][:to]
28
+ end
29
+
30
+ response = request(:get, credentials, "/invoices", :query => query)
31
+ api_model.parse(response.parsed_response)
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,21 @@
1
+ module Forest
2
+ module API
3
+ class Milestones < Base
4
+
5
+ # renamed from contant
6
+ # api_model Harvest::Contact
7
+
8
+ # include Harvest::Behavior::Crud
9
+
10
+ # def all(client_id = nil)
11
+ # response = if client_id
12
+ # request(:get, credentials, "/clients/#{client_id}/contacts")
13
+ # else
14
+ # request(:get, credentials, "/contacts")
15
+ # end
16
+
17
+ # api_model.parse(response.parsed_response)
18
+ # end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,23 @@
1
+ module Forecast
2
+ module API
3
+ class Projects < Base
4
+ api_model Forecast::Project
5
+
6
+ include Forecast::Behavior::Crud
7
+
8
+ def find_by_harvest_id(harvest_project_id)
9
+ selected = self.all.select{|x| x.harvest_id == harvest_project_id}
10
+
11
+ if selected.size > 1
12
+ raise "Forecasted::Error - more than 1 harvest_id forecast project"
13
+ else
14
+ return selected.first
15
+ end
16
+ end
17
+
18
+ def create ; raise "not implemented" ; end
19
+ def update ; raise "not implemented" ; end
20
+ def delete ; raise "not implemented" ; end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,53 @@
1
+ module Harvest
2
+ module API
3
+ class Reports < Base
4
+
5
+ TIME_FORMAT = '%Y%m%d'
6
+
7
+ def time_by_project(project, start_date, end_date, options = {})
8
+ query = { from: start_date.strftime(TIME_FORMAT), to: end_date.strftime(TIME_FORMAT) }
9
+ query[:user_id] = options.delete(:user).to_i if options[:user]
10
+ query[:billable] = (options.delete(:billable) ? "yes" : "no") unless options[:billable].nil?
11
+ query[:updated_since] = options.delete(:updated_since).to_s if options[:updated_since]
12
+ query.update(options)
13
+
14
+ response = request(:get, credentials, "/projects/#{project.to_i}/entries", query: query)
15
+ Harvest::TimeEntry.parse(JSON.parse(response.body).map {|h| h["day_entry"]})
16
+ end
17
+
18
+ def time_by_user(user, start_date, end_date, options = {})
19
+ query = { from: start_date.strftime(TIME_FORMAT), to: end_date.strftime(TIME_FORMAT) }
20
+ query[:project_id] = options.delete(:project).to_i if options[:project]
21
+ query[:billable] = (options.delete(:billable) ? "yes" : "no") unless options[:billable].nil?
22
+ query[:updated_since] = options.delete(:updated_since).to_s if options[:updated_since]
23
+ query.update(options)
24
+
25
+ response = request(:get, credentials, "/people/#{user.to_i}/entries", query: query)
26
+ Harvest::TimeEntry.parse(JSON.parse(response.body).map {|h| h["day_entry"]})
27
+ end
28
+
29
+ def expenses_by_user(user, start_date, end_date, options = {})
30
+ query = { from: start_date.strftime(TIME_FORMAT), to: end_date.strftime(TIME_FORMAT) }
31
+ query[:updated_since] = options.delete(:updated_since).to_s if options[:updated_since]
32
+ query.update(options)
33
+
34
+ response = request(:get, credentials, "/people/#{user.to_i}/expenses", query: query)
35
+ Harvest::Expense.parse(response.parsed_response)
36
+ end
37
+
38
+ def expenses_by_project(project, start_date, end_date, options = {})
39
+ query = { from: start_date.strftime(TIME_FORMAT), to: end_date.strftime(TIME_FORMAT) }
40
+ query[:updated_since] = options.delete(:updated_since).to_s if options[:updated_since]
41
+ query.update(options)
42
+
43
+ response = request(:get, credentials, "/projects/#{project.to_i}/expenses", query: query)
44
+ Harvest::Expense.parse(response.parsed_response)
45
+ end
46
+
47
+ def projects_by_client(client)
48
+ response = request(:get, credentials, "/projects?client=#{client.to_i}")
49
+ Harvest::Project.parse(response.parsed_response)
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,36 @@
1
+ module Harvest
2
+ module API
3
+ class Tasks < Base
4
+ api_model Harvest::Task
5
+
6
+ include Harvest::Behavior::Crud
7
+
8
+ # Deactivating tasks is not yet supported by the Harvest API.
9
+
10
+ # Deactivates the task. Does nothing if the task is already deactivated
11
+ #
12
+ # @param [Harvest::Task] task the task you want to deactivate
13
+ # @return [Harvest::Task] the deactivated task
14
+ #def deactivate(task)
15
+ # if task.active?
16
+ # request(:post, credentials, "#{api_model.api_path}/#{task.to_i}/deactivate", :headers => {'Content-Length' => '0'})
17
+ # task.active = false
18
+ # end
19
+ # task
20
+ #end
21
+
22
+ # Activates the task. Does nothing if the task is already activated
23
+ #
24
+ # @param [Harvest::Task] task the task you want to activate
25
+ # @return [Harvest::Task] the activated task
26
+ def activate(task)
27
+ if !task.active?
28
+ request(:post, credentials, "#{api_model.api_path}/#{task.to_i}/activate", :headers => {'Content-Length' => '0'})
29
+ task.active = true
30
+ end
31
+ task
32
+ end
33
+
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,48 @@
1
+ module Harvest
2
+ module API
3
+ class Time < Base
4
+
5
+ def find(id, user = nil)
6
+ response = request(:get, credentials, "/daily/show/#{id.to_i}", :query => of_user_query(user))
7
+ Harvest::TimeEntry.parse(response.parsed_response).first
8
+ end
9
+
10
+ def all(date = ::Time.now, user = nil)
11
+ Harvest::TimeEntry.parse(daily(date, user)["day_entries"])
12
+ end
13
+
14
+ def trackable_projects(date = ::Time.now, user = nil)
15
+ Harvest::TrackableProject.parse(daily(date, user)["projects"])
16
+ end
17
+
18
+ def toggle(id, user = nil)
19
+ response = request(:get, credentials, "/daily/timer/#{id}", :query => of_user_query(user))
20
+ Harvest::TimeEntry.parse(response.parsed_response).first
21
+ end
22
+
23
+ def create(entry, user = nil)
24
+ response = request(:post, credentials, '/daily/add', :body => entry.to_json, :query => of_user_query(user))
25
+ Harvest::TimeEntry.parse(response.parsed_response).first
26
+ end
27
+
28
+ def update(entry, user = nil)
29
+ request(:put, credentials, "/daily/update/#{entry.to_i}", :body => entry.to_json, :query => of_user_query(user))
30
+ find(entry.id, user)
31
+ end
32
+
33
+ def delete(entry, user = nil)
34
+ request(:delete, credentials, "/daily/delete/#{entry.to_i}", :query => of_user_query(user))
35
+ entry.id
36
+ end
37
+
38
+
39
+ private
40
+
41
+ def daily(date, user)
42
+ date = ::Time.parse(date) if String === date
43
+ response = request(:get, credentials, "/daily/#{date.yday}/#{date.year}", :query => of_user_query(user))
44
+ JSON.parse(response.body)
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,34 @@
1
+ module Harvest
2
+ module API
3
+ class UserAssignments < Base
4
+
5
+ def all(project)
6
+ response = request(:get, credentials, "/projects/#{project.to_i}/user_assignments")
7
+ Harvest::UserAssignment.parse(response.parsed_response)
8
+ end
9
+
10
+ def find(project, id)
11
+ response = request(:get, credentials, "/projects/#{project.to_i}/user_assignments/#{id}")
12
+ Harvest::UserAssignment.parse(response.parsed_response).first
13
+ end
14
+
15
+ def create(user_assignment)
16
+ user_assignment = Harvest::UserAssignment.wrap(user_assignment)
17
+ response = request(:post, credentials, "/projects/#{user_assignment.project_id}/user_assignments", :body => user_assignment.user_as_json.to_json)
18
+ id = response.headers["location"].match(/\/.*\/(\d+)\/.*\/(\d+)/)[2]
19
+ find(user_assignment.project_id, id)
20
+ end
21
+
22
+ def update(user_assignment)
23
+ user_assignment = Harvest::UserAssignment.wrap(user_assignment)
24
+ request(:put, credentials, "/projects/#{user_assignment.project_id}/user_assignments/#{user_assignment.id}", :body => user_assignment.to_json)
25
+ find(user_assignment.project_id, user_assignment.id)
26
+ end
27
+
28
+ def delete(user_assignment)
29
+ request(:delete, credentials, "/projects/#{user_assignment.project_id}/user_assignments/#{user_assignment.to_i}")
30
+ user_assignment.id
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,21 @@
1
+ module Harvest
2
+ module API
3
+ class Users < Base
4
+ api_model Harvest::User
5
+
6
+ include Harvest::Behavior::Crud
7
+ include Harvest::Behavior::Activatable
8
+
9
+ # Triggers Harvest to reset the user's password and sends them an email to change it.
10
+ # @overload reset_password(id)
11
+ # @param [Integer] id the id of the user you want to reset the password for
12
+ # @overload reset_password(user)
13
+ # @param [Harvest::User] user the user you want to reset the password for
14
+ # @return [Harvest::User] the user you passed in
15
+ def reset_password(user)
16
+ request(:post, credentials, "#{api_model.api_path}/#{user.to_i}/reset_password")
17
+ user
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,7 @@
1
+ module Forecast
2
+ class Assignment < Hashie::Mash
3
+ include Forecast::Model
4
+
5
+ api_path '/assignments'
6
+ end
7
+ end