harvested 0.3.0

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 (87) hide show
  1. data/.gitignore +23 -0
  2. data/HISTORY +16 -0
  3. data/MIT-LICENSE +20 -0
  4. data/README.md +67 -0
  5. data/Rakefile +52 -0
  6. data/VERSION +1 -0
  7. data/examples/basics.rb +35 -0
  8. data/examples/clear_account.rb +28 -0
  9. data/examples/task_assignments.rb +27 -0
  10. data/examples/user_assignments.rb +24 -0
  11. data/features/account.feature +7 -0
  12. data/features/client_contacts.feature +23 -0
  13. data/features/clients.feature +29 -0
  14. data/features/errors.feature +25 -0
  15. data/features/expense_categories.feature +21 -0
  16. data/features/expenses.feature +55 -0
  17. data/features/hardy_client.feature +40 -0
  18. data/features/projects.feature +39 -0
  19. data/features/reporting.feature +72 -0
  20. data/features/step_definitions/account_steps.rb +7 -0
  21. data/features/step_definitions/assignment_steps.rb +100 -0
  22. data/features/step_definitions/contact_steps.rb +11 -0
  23. data/features/step_definitions/debug_steps.rb +3 -0
  24. data/features/step_definitions/error_steps.rb +113 -0
  25. data/features/step_definitions/expenses_steps.rb +46 -0
  26. data/features/step_definitions/harvest_steps.rb +8 -0
  27. data/features/step_definitions/model_steps.rb +90 -0
  28. data/features/step_definitions/people_steps.rb +4 -0
  29. data/features/step_definitions/report_steps.rb +91 -0
  30. data/features/step_definitions/time_entry_steps.rb +40 -0
  31. data/features/support/env.rb +37 -0
  32. data/features/support/error_helpers.rb +18 -0
  33. data/features/support/fixtures/empty_clients.xml +2 -0
  34. data/features/support/fixtures/over_limit.xml +8 -0
  35. data/features/support/fixtures/receipt.png +0 -0
  36. data/features/support/fixtures/under_limit.xml +8 -0
  37. data/features/support/harvest_credentials.example.yml +4 -0
  38. data/features/support/harvest_helpers.rb +11 -0
  39. data/features/support/inflections.rb +9 -0
  40. data/features/task_assignment.feature +69 -0
  41. data/features/tasks.feature +25 -0
  42. data/features/time_tracking.feature +29 -0
  43. data/features/user_assignments.feature +33 -0
  44. data/features/users.feature +55 -0
  45. data/lib/harvest/api/account.rb +10 -0
  46. data/lib/harvest/api/base.rb +42 -0
  47. data/lib/harvest/api/clients.rb +10 -0
  48. data/lib/harvest/api/contacts.rb +19 -0
  49. data/lib/harvest/api/expense_categories.rb +9 -0
  50. data/lib/harvest/api/expenses.rb +28 -0
  51. data/lib/harvest/api/projects.rb +39 -0
  52. data/lib/harvest/api/reports.rb +39 -0
  53. data/lib/harvest/api/task_assignments.rb +32 -0
  54. data/lib/harvest/api/tasks.rb +9 -0
  55. data/lib/harvest/api/time.rb +32 -0
  56. data/lib/harvest/api/user_assignments.rb +32 -0
  57. data/lib/harvest/api/users.rb +15 -0
  58. data/lib/harvest/base.rb +59 -0
  59. data/lib/harvest/base_model.rb +34 -0
  60. data/lib/harvest/behavior/activatable.rb +21 -0
  61. data/lib/harvest/behavior/crud.rb +31 -0
  62. data/lib/harvest/client.rb +18 -0
  63. data/lib/harvest/contact.rb +16 -0
  64. data/lib/harvest/credentials.rb +21 -0
  65. data/lib/harvest/errors.rb +23 -0
  66. data/lib/harvest/expense.rb +19 -0
  67. data/lib/harvest/expense_category.rb +18 -0
  68. data/lib/harvest/hardy_client.rb +80 -0
  69. data/lib/harvest/project.rb +22 -0
  70. data/lib/harvest/rate_limit_status.rb +16 -0
  71. data/lib/harvest/task.rb +20 -0
  72. data/lib/harvest/task_assignment.rb +34 -0
  73. data/lib/harvest/time_entry.rb +41 -0
  74. data/lib/harvest/timezones.rb +150 -0
  75. data/lib/harvest/user.rb +40 -0
  76. data/lib/harvest/user_assignment.rb +34 -0
  77. data/lib/harvested.rb +31 -0
  78. data/spec/harvest/base_spec.rb +9 -0
  79. data/spec/harvest/credentials_spec.rb +22 -0
  80. data/spec/harvest/expense_spec.rb +15 -0
  81. data/spec/harvest/task_assignment_spec.rb +10 -0
  82. data/spec/harvest/time_entry_spec.rb +22 -0
  83. data/spec/harvest/user_assignment_spec.rb +10 -0
  84. data/spec/harvest/user_spec.rb +32 -0
  85. data/spec/spec.default.opts +1 -0
  86. data/spec/spec_helper.rb +10 -0
  87. metadata +243 -0
@@ -0,0 +1,33 @@
1
+ @clean
2
+ Feature: User Assignments
3
+
4
+ Scenario: Adding and Updating a User on a Project
5
+ Given I am using the credentials from "./support/harvest_credentials.yml"
6
+ When I create a client with the following:
7
+ | name | Client Projects |
8
+ | details | Building API Widgets across the country |
9
+ When I create a project for the client "Client Projects" with the following:
10
+ | name | Test Project |
11
+ | active | true |
12
+ | notes | project to test the api |
13
+ Then there should be a project "Test Project"
14
+ When I create a user with the following:
15
+ | first_name | Edgar |
16
+ | last_name | Ruth |
17
+ | email | edgar@ruth.com |
18
+ | password | mypassword |
19
+ | password_confirmation | mypassword |
20
+ Then there should be a user "edgar@ruth.com"
21
+ When I assign the user "edgar@ruth.com" to the project "Test Project"
22
+ Then the user "edgar@ruth.com" should be assigned to the project "Test Project"
23
+ When I update the user "edgar@ruth.com" on the project "Test Project" with the following:
24
+ | hourly_rate | 50.0 |
25
+ | project_manager | true |
26
+ Then the user "edgar@ruth.com" on the project "Test Project" should have the following attributes:
27
+ | active? | true |
28
+ | hourly_rate | 50.0 |
29
+ | project_manager | true |
30
+ When I remove the user "edgar@ruth.com" from the project "Test Project"
31
+ Then the user "edgar@ruth.com" should not be assigned to the project "Test Project"
32
+
33
+ Scenario: Removing a user from a project that has recorded hours
@@ -0,0 +1,55 @@
1
+ @clean
2
+ Feature: Managing People
3
+
4
+ Scenario: Adding and Removing a User
5
+ Given I am using the credentials from "./support/harvest_credentials.yml"
6
+ When I create a user with the following:
7
+ | first_name | Edgar |
8
+ | last_name | Ruth |
9
+ | email | edgar@ruth.com |
10
+ | password | mypassword |
11
+ | password_confirmation | mypassword |
12
+ | timezone | cst |
13
+ | admin | false |
14
+ | telephone | 444-4444 |
15
+ Then there should be a user "edgar@ruth.com"
16
+ When I update the user "edgar@ruth.com" with the following:
17
+ | first_name | Jonah |
18
+ | timezone | pst |
19
+ Then the user "edgar@ruth.com" should have the following attributes:
20
+ | first_name | Jonah |
21
+ | timezone | Pacific Time (US & Canada) |
22
+ When I delete the user "edgar@ruth.com"
23
+ Then there should not be a user "edgar@ruth.com"
24
+
25
+ Scenario: Activating and Deactivating a User
26
+ Given I am using the credentials from "./support/harvest_credentials.yml"
27
+ Then I create a user with the following:
28
+ | first_name | Simon |
29
+ | last_name | Steel |
30
+ | email | simon@steel.com |
31
+ | password | mypassword |
32
+ | password_confirmation | mypassword |
33
+ | timezone | cst |
34
+ | admin | false |
35
+ | telephone | 444-4444 |
36
+ Then the user "simon@steel.com" should be activated
37
+ When I deactivate the user "simon@steel.com"
38
+ Then the user "simon@steel.com" should be deactivated
39
+ When I activate the user "simon@steel.com"
40
+ Then the user "simon@steel.com" should be activated
41
+ Then I delete the user "simon@steel.com"
42
+
43
+ Scenario: Resetting a User's password
44
+ Given I am using the credentials from "./support/harvest_credentials.yml"
45
+ Then I create a user with the following:
46
+ | first_name | Edgar |
47
+ | last_name | Ruth |
48
+ | email | edgar@ruth.com |
49
+ | password | mypassword |
50
+ | password_confirmation | mypassword |
51
+ | timezone | cst |
52
+ | admin | false |
53
+ | telephone | 444-4444 |
54
+ Then there should be a user "edgar@ruth.com"
55
+ Then I reset the password of "edgar@ruth.com"
@@ -0,0 +1,10 @@
1
+ module Harvest
2
+ module API
3
+ class Account < Base
4
+ def rate_limit_status
5
+ response = request(:get, credentials, '/account/rate_limit_status')
6
+ Harvest::RateLimitStatus.parse(response.body)
7
+ end
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,42 @@
1
+ module Harvest
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
+ response = HTTParty.send(method, "#{credentials.host}#{path}", :query => options[:query], :body => options[:body], :headers => {"Accept" => "application/xml", "Content-Type" => "application/xml; charset=utf-8", "Authorization" => "Basic #{credentials.basic_auth}", "User-Agent" => "Harvestable/#{Harvest::VERSION}"}.update(options[:headers] || {}), :format => :plain)
23
+ case response.code
24
+ when 200..201
25
+ response
26
+ when 400
27
+ raise Harvest::BadRequest.new(response)
28
+ when 404
29
+ raise Harvest::NotFound.new(response)
30
+ when 500
31
+ raise Harvest::ServerError.new(response)
32
+ when 502
33
+ raise Harvest::Unavailable.new(response)
34
+ when 503
35
+ raise Harvest::RateLimited.new(response)
36
+ else
37
+ raise Harvest::InformHarvest.new(response)
38
+ end
39
+ end
40
+ end
41
+ end
42
+ 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,19 @@
1
+ module Harvest
2
+ module API
3
+ class Contacts < Base
4
+ api_model Harvest::Contact
5
+
6
+ include Harvest::Behavior::Crud
7
+
8
+ def all(client_id = nil)
9
+ response = if client_id
10
+ request(:get, credentials, "/clients/#{client_id}/contacts")
11
+ else
12
+ request(:get, credentials, "/contacts")
13
+ end
14
+
15
+ api_model.parse(response.body)
16
+ end
17
+ end
18
+ end
19
+ 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,28 @@
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)
9
+ date = ::Time.parse(date) if String === date
10
+ response = request(:get, credentials, "#{api_model.api_path}/#{date.yday}/#{date.year}")
11
+ api_model.parse(response.body)
12
+ end
13
+
14
+
15
+ # This is currently broken, but will come back to it
16
+ def attach(expense, filename, receipt)
17
+ body = ""
18
+ body << "------------------------------b7edea381b46\r\n"
19
+ body << %Q{Content-Disposition: form-data; name="expense[receipt]"; filename="#{filename}"\r\n}
20
+ body << "Content-Type: image/png\r\n"
21
+ body << "\r\n#{receipt.read}\r\n"
22
+ body << "------------------------------b7edea381b46\r\n"
23
+
24
+ request(:post, credentials, "#{api_model.api_path}/#{expense.to_i}/receipt", :headers => {'Content-Type' => 'multipart/form-data; boundary=------------------------------b7edea381b46'}, :body => body)
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,39 @@
1
+ module Harvest
2
+ module API
3
+ class Projects < Base
4
+ api_model Harvest::Project
5
+
6
+ include Harvest::Behavior::Crud
7
+
8
+ def create_task(project, task_name)
9
+ response = request(:post, credentials, "/projects/#{project.to_i}/task_assignments/add_with_create_new_task", :body => task_xml(task_name))
10
+ id = response.headers["location"].first.match(/\/.*\/(\d+)\/.*\/(\d+)/)[1]
11
+ find(id)
12
+ end
13
+
14
+ def deactivate(project)
15
+ if project.active?
16
+ request(:put, credentials, "#{api_model.api_path}/#{project.to_i}/toggle", :headers => {'Content-Length' => '0'})
17
+ project.active = false
18
+ end
19
+ project
20
+ end
21
+
22
+ def activate(project)
23
+ if !project.active?
24
+ request(:put, credentials, "#{api_model.api_path}/#{project.to_i}/toggle", :headers => {'Content-Length' => '0'})
25
+ project.active = true
26
+ end
27
+ project
28
+ end
29
+
30
+ private
31
+ def task_xml(name)
32
+ builder = Builder::XmlMarkup.new
33
+ builder.task do |t|
34
+ t.name(name)
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,39 @@
1
+ module Harvest
2
+ module API
3
+ class Reports < Base
4
+
5
+ def time_by_project(project, start_date, end_date, user = nil)
6
+ query = {:from => start_date.strftime("%Y%m%d"), :to => end_date.strftime("%Y%m%d")}
7
+ query[:user_id] = user.to_i if user
8
+
9
+ response = request(:get, credentials, "/projects/#{project.to_i}/entries", :query => query)
10
+ Harvest::TimeEntry.parse(massage_xml(response.body))
11
+ end
12
+
13
+ def time_by_user(user, start_date, end_date, project = nil)
14
+ query = {:from => start_date.strftime("%Y%m%d"), :to => end_date.strftime("%Y%m%d")}
15
+ query[:project_id] = project.to_i if project
16
+
17
+ response = request(:get, credentials, "/people/#{user.to_i}/entries", :query => query)
18
+ Harvest::TimeEntry.parse(massage_xml(response.body))
19
+ end
20
+
21
+ def expenses_by_user(user, start_date, end_date)
22
+ query = {:from => start_date.strftime("%Y%m%d"), :to => end_date.strftime("%Y%m%d")}
23
+
24
+ response = request(:get, credentials, "/people/#{user.to_i}/expenses", :query => query)
25
+ Harvest::Expense.parse(response.body)
26
+ end
27
+
28
+ private
29
+ def massage_xml(original_xml)
30
+ # this needs to be done because of the differences in dashes and underscores in the harvest api
31
+ xml = original_xml
32
+ %w(day-entry adjustment-record created-at project-id spent-at task-id timer-started-at updated-at user-id).each do |dash_field|
33
+ xml = xml.gsub(dash_field, dash_field.gsub("-", "_"))
34
+ end
35
+ xml
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,32 @@
1
+ module Harvest
2
+ module API
3
+ class TaskAssignments < Base
4
+
5
+ def all(project)
6
+ response = request(:get, credentials, "/projects/#{project.to_i}/task_assignments")
7
+ Harvest::TaskAssignment.parse(response.body)
8
+ end
9
+
10
+ def find(project, id)
11
+ response = request(:get, credentials, "/projects/#{project.to_i}/task_assignments/#{id}")
12
+ Harvest::TaskAssignment.parse(response.body, :single => true)
13
+ end
14
+
15
+ def create(task_assignment)
16
+ response = request(:post, credentials, "/projects/#{task_assignment.project_id}/task_assignments", :body => task_assignment.task_xml)
17
+ id = response.headers["location"].first.match(/\/.*\/(\d+)\/.*\/(\d+)/)[2]
18
+ find(task_assignment.project_id, id)
19
+ end
20
+
21
+ def update(task_assignment)
22
+ request(:put, credentials, "/projects/#{task_assignment.project_id}/task_assignments/#{task_assignment.to_i}", :body => task_assignment.to_xml)
23
+ find(task_assignment.project_id, task_assignment.id)
24
+ end
25
+
26
+ def delete(task_assignment)
27
+ request(:delete, credentials, "/projects/#{task_assignment.project_id}/task_assignments/#{task_assignment.to_i}")
28
+ task_assignment.id
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,9 @@
1
+ module Harvest
2
+ module API
3
+ class Tasks < Base
4
+ api_model Harvest::Task
5
+
6
+ include Harvest::Behavior::Crud
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,32 @@
1
+ module Harvest
2
+ module API
3
+ class Time < Base
4
+
5
+ def find(id)
6
+ response = request(:get, credentials, "/daily/show/#{id}")
7
+ Harvest::TimeEntry.parse(response.body, :single => true)
8
+ end
9
+
10
+ def all(date = ::Time.now)
11
+ date = ::Time.parse(date) if String === date
12
+ response = request(:get, credentials, "/daily/#{date.yday}/#{date.year}")
13
+ Harvest::TimeEntry.parse(response.body)
14
+ end
15
+
16
+ def create(entry)
17
+ response = request(:post, credentials, '/daily/add', :body => entry.to_xml)
18
+ Harvest::TimeEntry.parse(response.body).first
19
+ end
20
+
21
+ def update(entry)
22
+ request(:put, credentials, "/daily/update/#{entry.to_i}", :body => entry.to_xml)
23
+ find(entry.id)
24
+ end
25
+
26
+ def delete(entry)
27
+ request(:delete, credentials, "/daily/delete/#{entry.to_i}")
28
+ entry.id
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,32 @@
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.body)
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.body, :single => true)
13
+ end
14
+
15
+ def create(user_assignment)
16
+ response = request(:post, credentials, "/projects/#{user_assignment.project_id}/user_assignments", :body => user_assignment.user_xml)
17
+ id = response.headers["location"].first.match(/\/.*\/(\d+)\/.*\/(\d+)/)[2]
18
+ find(user_assignment.project_id, id)
19
+ end
20
+
21
+ def update(user_assignment)
22
+ request(:put, credentials, "/projects/#{user_assignment.project_id}/user_assignments/#{user_assignment.id}", :body => user_assignment.to_xml)
23
+ find(user_assignment.project_id, user_assignment.id)
24
+ end
25
+
26
+ def delete(user_assignment)
27
+ request(:delete, credentials, "/projects/#{user_assignment.project_id}/user_assignments/#{user_assignment.to_i}")
28
+ user_assignment.id
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,15 @@
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
+ def reset_password(user)
10
+ request(:post, credentials, "#{api_model.api_path}/#{user.to_i}/reset_password")
11
+ user
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,59 @@
1
+ module Harvest
2
+ class Base
3
+ attr_reader :request, :credentials
4
+
5
+ def initialize(subdomain, username, password, options = {})
6
+ options[:ssl] = true if options[:ssl].nil?
7
+ @credentials = Credentials.new(subdomain, username, password, options[:ssl])
8
+ raise InvalidCredentials unless credentials.valid?
9
+ end
10
+
11
+ def account
12
+ @account ||= Harvest::API::Account.new(credentials)
13
+ end
14
+
15
+ def clients
16
+ @clients ||= Harvest::API::Clients.new(credentials)
17
+ end
18
+
19
+ def contacts
20
+ @contacts ||= Harvest::API::Contacts.new(credentials)
21
+ end
22
+
23
+ def projects
24
+ @projects ||= Harvest::API::Projects.new(credentials)
25
+ end
26
+
27
+ def tasks
28
+ @tasks ||= Harvest::API::Tasks.new(credentials)
29
+ end
30
+
31
+ def users
32
+ @users ||= Harvest::API::Users.new(credentials)
33
+ end
34
+
35
+ def task_assignments
36
+ @task_assignments ||= Harvest::API::TaskAssignments.new(credentials)
37
+ end
38
+
39
+ def user_assignments
40
+ @user_assignments ||= Harvest::API::UserAssignments.new(credentials)
41
+ end
42
+
43
+ def expense_categories
44
+ @expense_categories ||= Harvest::API::ExpenseCategories.new(credentials)
45
+ end
46
+
47
+ def expenses
48
+ @expenses ||= Harvest::API::Expenses.new(credentials)
49
+ end
50
+
51
+ def time
52
+ @time ||= Harvest::API::Time.new(credentials)
53
+ end
54
+
55
+ def reports
56
+ @reports ||= Harvest::API::Reports.new(credentials)
57
+ end
58
+ end
59
+ end