harvested 0.3.3 → 0.4.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 (109) hide show
  1. data/Gemfile +15 -0
  2. data/HISTORY +11 -0
  3. data/README.md +14 -6
  4. data/Rakefile +13 -31
  5. data/TODO +2 -0
  6. data/VERSION +1 -1
  7. data/examples/user_assignments.rb +1 -1
  8. data/harvested.gemspec +114 -126
  9. data/lib/ext/array.rb +52 -0
  10. data/lib/ext/date.rb +9 -0
  11. data/lib/ext/hash.rb +17 -0
  12. data/lib/ext/time.rb +5 -0
  13. data/lib/harvest/api/account.rb +8 -1
  14. data/lib/harvest/api/base.rb +32 -10
  15. data/lib/harvest/api/contacts.rb +1 -1
  16. data/lib/harvest/api/expenses.rb +3 -4
  17. data/lib/harvest/api/invoice_categories.rb +26 -0
  18. data/lib/harvest/api/invoices.rb +16 -0
  19. data/lib/harvest/api/projects.rb +2 -10
  20. data/lib/harvest/api/reports.rb +13 -16
  21. data/lib/harvest/api/task_assignments.rb +8 -6
  22. data/lib/harvest/api/tasks.rb +1 -1
  23. data/lib/harvest/api/time.rb +13 -13
  24. data/lib/harvest/api/user_assignments.rb +7 -5
  25. data/lib/harvest/base.rb +9 -1
  26. data/lib/harvest/behavior/activatable.rb +2 -2
  27. data/lib/harvest/behavior/crud.rb +15 -13
  28. data/lib/harvest/client.rb +18 -13
  29. data/lib/harvest/contact.rb +13 -11
  30. data/lib/harvest/errors.rb +6 -4
  31. data/lib/harvest/expense.rb +40 -14
  32. data/lib/harvest/expense_category.rb +10 -9
  33. data/lib/harvest/hardy_client.rb +1 -1
  34. data/lib/harvest/invoice.rb +103 -0
  35. data/lib/harvest/invoice_category.rb +18 -0
  36. data/lib/harvest/line_item.rb +12 -0
  37. data/lib/harvest/model.rb +120 -0
  38. data/lib/harvest/project.rb +55 -26
  39. data/lib/harvest/rate_limit_status.rb +9 -8
  40. data/lib/harvest/task.rb +17 -14
  41. data/lib/harvest/task_assignment.rb +27 -22
  42. data/lib/harvest/time_entry.rb +32 -30
  43. data/lib/harvest/user.rb +46 -22
  44. data/lib/harvest/user_assignment.rb +24 -17
  45. data/lib/harvested.rb +12 -5
  46. data/spec/functional/account_spec.rb +17 -0
  47. data/spec/functional/clients_spec.rb +58 -0
  48. data/spec/functional/errors_spec.rb +22 -0
  49. data/spec/functional/expenses_spec.rb +84 -0
  50. data/spec/functional/hardy_client_spec.rb +33 -0
  51. data/spec/functional/invoice_spec.rb +67 -0
  52. data/spec/functional/project_spec.rb +50 -0
  53. data/spec/functional/reporting_spec.rb +80 -0
  54. data/spec/functional/tasks_spec.rb +88 -0
  55. data/spec/functional/time_tracking_spec.rb +53 -0
  56. data/spec/functional/users_spec.rb +102 -0
  57. data/spec/harvest/base_spec.rb +1 -1
  58. data/spec/harvest/credentials_spec.rb +1 -1
  59. data/spec/harvest/expense_category_spec.rb +5 -0
  60. data/spec/harvest/expense_spec.rb +8 -5
  61. data/spec/harvest/invoice_spec.rb +47 -0
  62. data/spec/harvest/project_spec.rb +11 -0
  63. data/spec/harvest/task_assignment_spec.rb +4 -4
  64. data/spec/harvest/task_spec.rb +7 -0
  65. data/spec/harvest/time_entry_spec.rb +11 -10
  66. data/spec/harvest/user_assignment_spec.rb +3 -3
  67. data/spec/harvest/user_spec.rb +3 -1
  68. data/spec/spec_helper.rb +37 -6
  69. data/{features → spec}/support/harvest_credentials.example.yml +0 -1
  70. data/spec/support/harvested_helpers.rb +44 -0
  71. data/spec/support/json_examples.rb +11 -0
  72. data/spec/test_rubies +5 -0
  73. metadata +109 -85
  74. data/.gitignore +0 -28
  75. data/features/account.feature +0 -7
  76. data/features/client_contacts.feature +0 -23
  77. data/features/clients.feature +0 -29
  78. data/features/errors.feature +0 -25
  79. data/features/expense_categories.feature +0 -21
  80. data/features/expenses.feature +0 -55
  81. data/features/hardy_client.feature +0 -40
  82. data/features/projects.feature +0 -39
  83. data/features/reporting.feature +0 -72
  84. data/features/step_definitions/account_steps.rb +0 -7
  85. data/features/step_definitions/assignment_steps.rb +0 -100
  86. data/features/step_definitions/contact_steps.rb +0 -11
  87. data/features/step_definitions/debug_steps.rb +0 -3
  88. data/features/step_definitions/error_steps.rb +0 -113
  89. data/features/step_definitions/expenses_steps.rb +0 -46
  90. data/features/step_definitions/harvest_steps.rb +0 -8
  91. data/features/step_definitions/model_steps.rb +0 -90
  92. data/features/step_definitions/people_steps.rb +0 -4
  93. data/features/step_definitions/report_steps.rb +0 -91
  94. data/features/step_definitions/time_entry_steps.rb +0 -40
  95. data/features/support/env.rb +0 -37
  96. data/features/support/error_helpers.rb +0 -18
  97. data/features/support/fixtures/empty_clients.xml +0 -2
  98. data/features/support/fixtures/over_limit.xml +0 -8
  99. data/features/support/fixtures/receipt.png +0 -0
  100. data/features/support/fixtures/under_limit.xml +0 -8
  101. data/features/support/harvest_helpers.rb +0 -11
  102. data/features/support/inflections.rb +0 -9
  103. data/features/task_assignment.feature +0 -69
  104. data/features/tasks.feature +0 -25
  105. data/features/time_tracking.feature +0 -29
  106. data/features/user_assignments.feature +0 -33
  107. data/features/users.feature +0 -55
  108. data/lib/harvest/base_model.rb +0 -73
  109. data/spec/spec.default.opts +0 -1
@@ -1,5 +1,5 @@
1
1
  module Harvest
2
-
2
+
3
3
  # The model that contains information about a project
4
4
  #
5
5
  # == Fields
@@ -22,35 +22,64 @@ module Harvest
22
22
  # [+active_task_assignments_count+] (READONLY) the number of active task assignments
23
23
  # [+created_at+] (READONLY) when the project was created
24
24
  # [+updated_at+] (READONLY) when the project was updated
25
- class Project < BaseModel
26
- include HappyMapper
27
-
25
+ class Project < Hashie::Dash
26
+ include Harvest::Model
27
+
28
28
  api_path '/projects'
29
-
30
- element :id, Integer
31
- element :client_id, Integer, :tag => 'client-id'
32
- element :name, String
33
- element :code, String
34
- element :notes, String
35
- element :fees, String
36
- element :active, Boolean
37
- element :billable, Boolean
38
- element :budget, Float
39
- element :budget_by, String, :tag => 'budget-by'
40
- element :hourly_rate, Float, :tag => 'hourly-rate'
41
- element :bill_by, String, :tag => 'bill-by'
42
- element :created_at, Time, :tag => 'created-at'
43
- element :updated_at, Time, :tag => 'updated-at'
44
- element :notify_when_over_budget, Boolean, :tag => 'notify-when-over-budget'
45
- element :over_budget_notification_percentage, Float, :tag => 'over-budget-notification-percentage'
46
- element :show_budget_to_all, Boolean, :tag => 'show-budget-to-all'
47
- element :basecamp_id, Integer, :tag => 'basecamp-id'
48
- element :highrise_deal_id, Integer, :tag => 'highrise-deal-id'
49
- element :active_task_assignments_count, Integer, :tag => 'active-task-assignments-count'
50
-
29
+
30
+ property :id
31
+ property :client_id
32
+ property :name
33
+ property :code
34
+ property :notes
35
+ property :fees
36
+ property :active
37
+ property :billable
38
+ property :budget
39
+ property :budget_by
40
+ property :hourly_rate
41
+ property :bill_by
42
+ property :created_at
43
+ property :updated_at
44
+ property :notify_when_over_budget
45
+ property :over_budget_notification_percentage
46
+ property :show_budget_to_all
47
+ property :basecamp_id
48
+ property :highrise_deal_id
49
+ property :active_task_assignments_count
50
+ property :over_budget_notified_at
51
+ property :earliest_record_at
52
+ property :cost_budget
53
+ property :cost_budget_include_expenses
54
+ property :latest_record_at
55
+ property :estimate_by
56
+ property :hint_earliest_record_at
57
+ property :hint_latest_record_at
58
+ property :active_user_assignments_count
59
+ property :cache_version
60
+ property :estimate
61
+
51
62
  alias_method :active?, :active
52
63
  alias_method :billable?, :billable
53
64
  alias_method :notify_when_over_budget?, :notify_when_over_budget
54
65
  alias_method :show_budget_to_all?, :show_budget_to_all
66
+
67
+ def as_json(args = {})
68
+ super(args).tap do |json|
69
+ json[json_root].delete("hint_earliest_record_at")
70
+ json[json_root].delete("hint_latest_record_at")
71
+ end
72
+ end
73
+
74
+ def self.parse(json)
75
+ json = String === json ? JSON.parse(json) : json
76
+ Array.wrap(json).each do |attrs|
77
+ # need to cleanup some attributes
78
+ project_attrs = attrs[json_root] || {}
79
+ project_attrs["hint_latest_record_at"] = project_attrs.delete("hint-latest-record-at")
80
+ project_attrs["hint_earliest_record_at"] = project_attrs.delete("hint-earliest-record-at")
81
+ end
82
+ super(json)
83
+ end
55
84
  end
56
85
  end
@@ -8,15 +8,16 @@ module Harvest
8
8
  # [+timeframe_limit+] The amount of seconds before a rate limit refresh occurs
9
9
  # [+max_calls+] The number of requests you can make within the +timeframe_limit+
10
10
  # [+lockout_seconds+] If you exceed the rate limit, how long you will be locked out from Harvest
11
- class RateLimitStatus < BaseModel
12
- include HappyMapper
11
+ class RateLimitStatus < Hashie::Dash
12
+ include Harvest::Model
13
13
 
14
- tag 'hash'
15
- element :last_access_at, Time, :tag => 'last-access-at'
16
- element :count, Integer
17
- element :timeframe_limit, Integer, :tag => 'timeframe-limit'
18
- element :max_calls, Integer, :tag => 'max-calls'
19
- element :lockout_seconds, Integer, :tag => 'lockout-seconds'
14
+ skip_json_root true
15
+
16
+ property :last_access_at
17
+ property :count
18
+ property :timeframe_limit
19
+ property :max_calls
20
+ property :lockout_seconds
20
21
 
21
22
  # Returns true if the user is over their rate limit
22
23
  # @return [Boolean]
@@ -1,5 +1,5 @@
1
1
  module Harvest
2
-
2
+
3
3
  # The model that contains information about a task
4
4
  #
5
5
  # == Fields
@@ -9,22 +9,25 @@ module Harvest
9
9
  # [+deactivated+] whether the task is deactivated
10
10
  # [+hourly_rate+] what the default hourly rate for the task is
11
11
  # [+default?+] whether to add this task to new projects by default
12
- class Task < BaseModel
13
- include HappyMapper
14
-
12
+ class Task < Hashie::Dash
13
+ include Harvest::Model
14
+
15
15
  api_path '/tasks'
16
-
17
- element :id, Integer
18
- element :name, String
19
- element :billable, Boolean, :tag => 'billable-by-default'
20
- element :deactivated, Boolean, :tag => 'deactivated'
21
- element :hourly_rate, Float, :tag => 'default-hourly-rate'
22
- element :default, Boolean, :tag => 'is-default'
23
-
16
+
17
+ property :id
18
+ property :name
19
+ property :billable_by_default
20
+ property :deactivated
21
+ property :default_hourly_rate
22
+ property :is_default
23
+ property :created_at
24
+ property :updated_at
25
+ property :cache_version
26
+
24
27
  def active?
25
28
  !deactivated
26
29
  end
27
-
28
- alias_method :default?, :default
30
+
31
+ alias_method :default?, :is_default
29
32
  end
30
33
  end
@@ -1,36 +1,41 @@
1
1
  module Harvest
2
- class TaskAssignment < BaseModel
3
- include HappyMapper
2
+ class TaskAssignment < Hashie::Dash
3
+ include Harvest::Model
4
+
5
+ property :id
6
+ property :task_id
7
+ property :project_id
8
+ property :billable
9
+ property :deactivated
10
+ property :hourly_rate
11
+ property :budget
12
+ property :estimate
13
+ property :created_at
14
+ property :updated_at
4
15
 
5
- tag 'task-assignment'
6
- element :id, Integer
7
- element :task_id, Integer, :tag => 'task-id'
8
- element :project_id, Integer, :tag => 'project-id'
9
- element :billable, Boolean
10
- element :deactivated, Boolean
11
- element :hourly_rate, Float, :tag => 'hourly-rate'
12
- element :budget, Float
13
- element :estimate, Float
16
+ def initialize(args = {})
17
+ args = args.stringify_keys
18
+ self.task = args.delete("task") if args["task"]
19
+ self.project = args.delete("project") if args["project"]
20
+ super
21
+ end
14
22
 
15
23
  def task=(task)
16
- @task_id = task.to_i
24
+ self["task_id"] = task.to_i
17
25
  end
18
-
26
+
19
27
  def project=(project)
20
- @project_id = project.to_i
28
+ self["project_id"] = project.to_i
21
29
  end
22
-
30
+
23
31
  def active?
24
32
  !deactivated
25
33
  end
26
-
27
- def task_xml
28
- builder = Builder::XmlMarkup.new
29
- builder.task do |t|
30
- t.id(task_id)
31
- end
34
+
35
+ def task_as_json
36
+ {"task" => {"id" => task_id}}
32
37
  end
33
-
38
+
34
39
  alias_method :billable?, :billable
35
40
  end
36
41
  end
@@ -1,43 +1,45 @@
1
1
  module Harvest
2
- class TimeEntry < BaseModel
3
- include HappyMapper
2
+ class TimeEntry < Hashie::Dash
3
+ include Harvest::Model
4
4
 
5
- tag 'day_entry'
5
+ property :id
6
+ property :client
7
+ property :hours
8
+ property :notes
6
9
 
7
- element :id, Integer
8
- element :client, String
9
- element :project, String
10
- element :task, String
11
- element :hours, Float
12
- element :notes, String
10
+ property :project_id
11
+ property :task_id
12
+ property :project
13
+ property :task
14
+ property :spent_at
15
+ property :created_at
16
+ property :updated_at
17
+ property :user_id
18
+ property :of_user
19
+ property :is_closed
20
+ property :is_billed
21
+ property :timer_started_at
22
+ property :adjustment_record
13
23
 
14
- element :project_id, Integer
15
- element :task_id, Integer
16
- element :spent_at, Time
17
- element :created_at, Time
18
- element :updated_at, Time
19
- element :user_id, Integer
20
- element :closed, Boolean, :tag => 'is-closed'
21
- element :billed, Boolean, :tag => 'is-billed'
22
- element :of_user, Integer
24
+ skip_json_root true
25
+
26
+ def initialize(args = {})
27
+ args = args.stringify_keys
28
+ self.spent_at = args.delete("spent_at") if args["spent_at"]
29
+ super
30
+ end
23
31
 
24
32
  def spent_at=(date)
25
- @spent_at = (String === date ? Time.parse(date) : date)
33
+ self["spent_at"] = (String === date ? Time.parse(date) : date)
26
34
  end
27
35
 
28
- def to_xml
29
- builder = Builder::XmlMarkup.new
30
- builder.request do |r|
31
- r.tag!('notes', notes) if notes
32
- r.tag!('hours', hours) if hours
33
- r.tag!('project_id', project_id) if project_id
34
- r.tag!('task_id', task_id) if task_id
35
- r.tag!('spent_at', spent_at) if spent_at
36
- r.tag!('of_user', of_user) if of_user
36
+ def as_json(args = {})
37
+ super(args).stringify_keys.tap do |hash|
38
+ hash.update("spent_at" => (spent_at.nil? ? nil : spent_at.to_time.xmlschema))
37
39
  end
38
40
  end
39
41
 
40
- alias_method :closed?, :closed
41
- alias_method :billed?, :billed
42
+ alias_method :closed?, :is_closed
43
+ alias_method :billed?, :is_billed
42
44
  end
43
45
  end
@@ -9,35 +9,59 @@ module Harvest
9
9
  # [+last_name+] the last name for the user
10
10
  # [+telephone+] the telephone for the user
11
11
  # [+department] the department for the user
12
- # [+password|password_confirmation+] the password for the user (only used on create.)
13
12
  # [+has_access_to_all_future_projects+] whether the user should be added to future projects by default
14
13
  # [+hourly_rate+] what the default hourly rate for the user is
15
14
  # [+admin?+] whether the user is an admin
16
15
  # [+contractor?+] whether the user is a contractor
17
16
  # [+contractor?+] whether the user is a contractor
18
17
  # [+timezone+] the timezone for the user.
19
- class User < BaseModel
20
- include HappyMapper
18
+ class User < Hashie::Dash
19
+ include Harvest::Model
21
20
 
22
21
  api_path '/people'
23
- element :id, Integer
24
- element :email, String
25
- element :first_name, String, :tag => 'first-name'
26
- element :last_name, String, :tag => 'last-name'
27
- element :has_access_to_all_future_projects, Boolean, :tag => 'has-access-to-all-future-projects'
28
- element :hourly_rate, Float, :tag => 'default-hourly-rate'
29
- element :active, Boolean, :tag => 'is-active'
30
- element :admin, Boolean, :tag => 'is-admin'
31
- element :contractor, Boolean, :tag => 'is-contractor'
32
- element :telephone, String
33
- element :department, String
34
- element :timezone, String
35
- element :password, String
36
- element :password_confirmation, String, :tag => 'password-confirmation'
22
+ property :id
23
+ property :email
24
+ property :first_name
25
+ property :last_name
26
+ property :has_access_to_all_future_projects
27
+ property :default_hourly_rate
28
+ property :is_active
29
+ property :is_admin
30
+ property :is_contractor
31
+ property :telephone
32
+ property :department
33
+ property :timezone
34
+ property :password
35
+ property :first_timer
36
+ property :wants_newsletter
37
+ property :preferred_project_status_reports_screen
38
+ property :preferred_approval_screen
39
+ property :created_at
40
+ property :updated_at
41
+ property :twitter_username
42
+ property :preferred_entry_method
43
+ property :default_time_project_id
44
+ property :default_task_id
45
+ property :default_expense_category_id
46
+ property :opensocial_identifier
47
+ property :duplicate_timesheet_wants_notes
48
+ property :wants_timesheet_duplication
49
+ property :cache_version
50
+ property :email_after_submit
51
+ property :default_expense_project_id
52
+ property :identity_url
53
+ property :timestamp_timers
37
54
 
38
- alias_method :active?, :active
39
- alias_method :admin?, :admin
40
- alias_method :contractor?, :contractor
55
+ alias_method :active?, :is_active
56
+ alias_method :admin?, :is_admin
57
+ alias_method :contractor?, :is_contractor
58
+
59
+ def initialize(args = {})
60
+ args = args.stringify_keys
61
+ args["is_admin"] = args.delete("admin") if args["admin"]
62
+ self.timezone = args.delete("timezone") if args["timezone"]
63
+ super
64
+ end
41
65
 
42
66
  # Sets the timezone for the user. This can be done in a variety of ways.
43
67
  #
@@ -56,9 +80,9 @@ module Harvest
56
80
  when 'pst', 'pdt' then self.timezone = 'america/los_angeles'
57
81
  else
58
82
  if Harvest::Timezones::MAPPING[tz]
59
- @timezone = Harvest::Timezones::MAPPING[tz]
83
+ self["timezone"] = Harvest::Timezones::MAPPING[tz]
60
84
  else
61
- @timezone = timezone
85
+ self["timezone"] = timezone
62
86
  end
63
87
  end
64
88
  end
@@ -1,34 +1,41 @@
1
1
  module Harvest
2
- class UserAssignment < BaseModel
3
- include HappyMapper
2
+ class UserAssignment < Hashie::Dash
3
+ include Harvest::Model
4
4
 
5
- tag 'user-assignment'
6
- element :id, Integer
7
- element :user_id, Integer, :tag => 'user-id'
8
- element :project_id, Integer, :tag => 'project-id'
9
- element :deactivated, Boolean
10
- element :project_manager, Boolean, :tag => 'is-project-manager'
11
- element :hourly_rate, Float, :tag => 'hourly-rate'
5
+ property :id
6
+ property :user_id
7
+ property :project_id
8
+ property :deactivated
9
+ property :is_project_manager
10
+ property :hourly_rate
11
+ property :created_at
12
+ property :updated_at
13
+ property :budget
14
+ property :estimate
15
+
16
+ def initialize(args = {})
17
+ args = args.stringify_keys
18
+ self.user = args.delete("user") if args["user"]
19
+ self.project = args.delete("project") if args["project"]
20
+ super
21
+ end
12
22
 
13
23
  def user=(user)
14
- @user_id = user.to_i
24
+ self["user_id"] = user.to_i
15
25
  end
16
26
 
17
27
  def project=(project)
18
- @project_id = project.to_i
28
+ self["project_id"] = project.to_i
19
29
  end
20
30
 
21
31
  def active?
22
32
  !deactivated
23
33
  end
24
34
 
25
- def user_xml
26
- builder = Builder::XmlMarkup.new
27
- builder.user do |t|
28
- t.id(user_id)
29
- end
35
+ def user_as_json
36
+ {"user" => {"id" => user_id}}
30
37
  end
31
38
 
32
- alias_method :project_manager?, :project_manager
39
+ alias_method :project_manager?, :is_project_manager
33
40
  end
34
41
  end