forecasted 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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