quilted-harvested 0.3.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (89) hide show
  1. data/.document +3 -0
  2. data/.gitignore +25 -0
  3. data/HISTORY +19 -0
  4. data/MIT-LICENSE +20 -0
  5. data/README.md +68 -0
  6. data/Rakefile +25 -0
  7. data/VERSION +1 -0
  8. data/examples/basics.rb +35 -0
  9. data/examples/clear_account.rb +28 -0
  10. data/examples/task_assignments.rb +27 -0
  11. data/examples/user_assignments.rb +24 -0
  12. data/features/account.feature +7 -0
  13. data/features/client_contacts.feature +23 -0
  14. data/features/clients.feature +29 -0
  15. data/features/errors.feature +25 -0
  16. data/features/expense_categories.feature +21 -0
  17. data/features/expenses.feature +55 -0
  18. data/features/hardy_client.feature +40 -0
  19. data/features/projects.feature +39 -0
  20. data/features/reporting.feature +72 -0
  21. data/features/step_definitions/account_steps.rb +7 -0
  22. data/features/step_definitions/assignment_steps.rb +100 -0
  23. data/features/step_definitions/contact_steps.rb +11 -0
  24. data/features/step_definitions/debug_steps.rb +3 -0
  25. data/features/step_definitions/error_steps.rb +113 -0
  26. data/features/step_definitions/expenses_steps.rb +46 -0
  27. data/features/step_definitions/harvest_steps.rb +8 -0
  28. data/features/step_definitions/model_steps.rb +90 -0
  29. data/features/step_definitions/people_steps.rb +4 -0
  30. data/features/step_definitions/report_steps.rb +91 -0
  31. data/features/step_definitions/time_entry_steps.rb +40 -0
  32. data/features/support/env.rb +37 -0
  33. data/features/support/error_helpers.rb +18 -0
  34. data/features/support/fixtures/empty_clients.xml +2 -0
  35. data/features/support/fixtures/over_limit.xml +8 -0
  36. data/features/support/fixtures/receipt.png +0 -0
  37. data/features/support/fixtures/under_limit.xml +8 -0
  38. data/features/support/harvest_credentials.example.yml +4 -0
  39. data/features/support/harvest_helpers.rb +11 -0
  40. data/features/support/inflections.rb +9 -0
  41. data/features/task_assignment.feature +69 -0
  42. data/features/tasks.feature +25 -0
  43. data/features/time_tracking.feature +29 -0
  44. data/features/user_assignments.feature +33 -0
  45. data/features/users.feature +55 -0
  46. data/harvested.gemspec +159 -0
  47. data/lib/harvest/api/account.rb +15 -0
  48. data/lib/harvest/api/base.rb +42 -0
  49. data/lib/harvest/api/clients.rb +10 -0
  50. data/lib/harvest/api/contacts.rb +19 -0
  51. data/lib/harvest/api/expense_categories.rb +9 -0
  52. data/lib/harvest/api/expenses.rb +28 -0
  53. data/lib/harvest/api/projects.rb +54 -0
  54. data/lib/harvest/api/reports.rb +39 -0
  55. data/lib/harvest/api/task_assignments.rb +32 -0
  56. data/lib/harvest/api/tasks.rb +9 -0
  57. data/lib/harvest/api/time.rb +32 -0
  58. data/lib/harvest/api/user_assignments.rb +32 -0
  59. data/lib/harvest/api/users.rb +21 -0
  60. data/lib/harvest/base.rb +258 -0
  61. data/lib/harvest/base_model.rb +73 -0
  62. data/lib/harvest/behavior/activatable.rb +31 -0
  63. data/lib/harvest/behavior/crud.rb +57 -0
  64. data/lib/harvest/client.rb +30 -0
  65. data/lib/harvest/contact.rb +29 -0
  66. data/lib/harvest/credentials.rb +21 -0
  67. data/lib/harvest/errors.rb +23 -0
  68. data/lib/harvest/expense.rb +19 -0
  69. data/lib/harvest/expense_category.rb +18 -0
  70. data/lib/harvest/hardy_client.rb +80 -0
  71. data/lib/harvest/project.rb +56 -0
  72. data/lib/harvest/rate_limit_status.rb +28 -0
  73. data/lib/harvest/task.rb +30 -0
  74. data/lib/harvest/task_assignment.rb +34 -0
  75. data/lib/harvest/time_entry.rb +42 -0
  76. data/lib/harvest/timezones.rb +149 -0
  77. data/lib/harvest/user.rb +66 -0
  78. data/lib/harvest/user_assignment.rb +34 -0
  79. data/lib/harvested.rb +62 -0
  80. data/spec/harvest/base_spec.rb +9 -0
  81. data/spec/harvest/credentials_spec.rb +22 -0
  82. data/spec/harvest/expense_spec.rb +15 -0
  83. data/spec/harvest/task_assignment_spec.rb +10 -0
  84. data/spec/harvest/time_entry_spec.rb +22 -0
  85. data/spec/harvest/user_assignment_spec.rb +10 -0
  86. data/spec/harvest/user_spec.rb +32 -0
  87. data/spec/spec.default.opts +1 -0
  88. data/spec/spec_helper.rb +10 -0
  89. metadata +264 -0
@@ -0,0 +1,80 @@
1
+ module Harvest
2
+ class HardyClient < Delegator
3
+ def initialize(client, max_retries)
4
+ super(client)
5
+ @_sd_obj = @client = client
6
+ @max_retries = max_retries
7
+ (@client.public_methods - Object.public_instance_methods).each do |name|
8
+ instance_eval <<-END
9
+ def #{name}(*args)
10
+ wrap_collection do
11
+ @client.send('#{name}', *args)
12
+ end
13
+ end
14
+ END
15
+ end
16
+ end
17
+
18
+ def __getobj__; @_sd_obj; end
19
+ def __setobj__(obj); @_sd_obj = obj; end
20
+
21
+ def wrap_collection
22
+ collection = yield
23
+ HardyCollection.new(collection, self, @max_retries)
24
+ end
25
+
26
+ class HardyCollection < Delegator
27
+ def initialize(collection, client, max_retries)
28
+ super(collection)
29
+ @_sd_obj = @collection = collection
30
+ @client = client
31
+ @max_retries = max_retries
32
+ (@collection.public_methods - Object.public_instance_methods).each do |name|
33
+ instance_eval <<-END
34
+ def #{name}(*args)
35
+ retry_rate_limits do
36
+ @collection.send('#{name}', *args)
37
+ end
38
+ end
39
+ END
40
+ end
41
+ end
42
+
43
+ def __getobj__; @_sd_obj; end
44
+ def __setobj__(obj); @_sd_obj = obj; end
45
+
46
+ def retry_rate_limits
47
+ retries = 0
48
+
49
+ retry_func = lambda do |e|
50
+ if retries < @max_retries
51
+ retries += 1
52
+ true
53
+ else
54
+ raise e
55
+ end
56
+ end
57
+
58
+ begin
59
+ yield
60
+ rescue Harvest::RateLimited => e
61
+ seconds = if e.response.headers["retry-after"]
62
+ e.response.headers["retry-after"].first.to_i
63
+ else
64
+ 16
65
+ end
66
+ sleep(seconds)
67
+ retry
68
+ rescue Harvest::Unavailable, Harvest::InformHarvest => e
69
+ would_retry = retry_func.call(e)
70
+ sleep(16) if @client.account.rate_limit_status.over_limit?
71
+ retry if would_retry
72
+ rescue Net::HTTPError, Net::HTTPFatalError => e
73
+ retry if retry_func.call(e)
74
+ rescue SystemCallError => e
75
+ retry if e.is_a?(Errno::ECONNRESET) && retry_func.call(e)
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,56 @@
1
+ module Harvest
2
+
3
+ # The model that contains information about a project
4
+ #
5
+ # == Fields
6
+ # [+id+] (READONLY) the id of the project
7
+ # [+name+] (REQUIRED) the name of the project
8
+ # [+client_id+] (REQUIRED) the client id of the project
9
+ # [+code+] the project code
10
+ # [+notes+] the project notes
11
+ # [+active?+] true|false whether the project is active
12
+ # [+billable?+] true|false where the project is billable
13
+ # [+budget_by+] how the budget is calculated for the project +project|project_cost|task|person|nil+
14
+ # [+budget+] what the budget is for the project (based on budget_by)
15
+ # [+bill_by+] how to bill the project +Tasks|People|Project|nil+
16
+ # [+hourly_rate+] what the hourly rate for the project is based on +bill_by+
17
+ # [+notify_when_over_budget?+] whether the project will send notifications when it goes over budget
18
+ # [+over_budget_notification_percentage+] what percentage of the budget the project has to be before it sends a notification. Based on +notify_when_over_budget?+
19
+ # [+show_budget_to_all?+] whether the project's budget is shown to employees and contractors
20
+ # [+basecamp_id+] (READONLY) the id of the basecamp project associated to the project
21
+ # [+highrise_deal_id+] (READONLY) the id of the highrise deal associated to the project
22
+ # [+active_task_assignments_count+] (READONLY) the number of active task assignments
23
+ # [+created_at+] (READONLY) when the project was created
24
+ # [+updated_at+] (READONLY) when the project was updated
25
+ class Project < BaseModel
26
+ include HappyMapper
27
+
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
+
51
+ alias_method :active?, :active
52
+ alias_method :billable?, :billable
53
+ alias_method :notify_when_over_budget?, :notify_when_over_budget
54
+ alias_method :show_budget_to_all?, :show_budget_to_all
55
+ end
56
+ end
@@ -0,0 +1,28 @@
1
+ module Harvest
2
+
3
+ # The model that contains the information about the user's rate limit
4
+ #
5
+ # == Fields
6
+ # [+last_access_at+] The last registered request
7
+ # [+count+] The current number of requests registered
8
+ # [+timeframe_limit+] The amount of seconds before a rate limit refresh occurs
9
+ # [+max_calls+] The number of requests you can make within the +timeframe_limit+
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
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'
20
+
21
+ # Returns true if the user is over their rate limit
22
+ # @return [Boolean]
23
+ # @see http://www.getharvest.com/api
24
+ def over_limit?
25
+ count > max_calls
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,30 @@
1
+ module Harvest
2
+
3
+ # The model that contains information about a task
4
+ #
5
+ # == Fields
6
+ # [+id+] (READONLY) the id of the task
7
+ # [+name+] (REQUIRED) the name of the task
8
+ # [+billable+] whether the task is billable by default
9
+ # [+deactivated+] whether the task is deactivated
10
+ # [+hourly_rate+] what the default hourly rate for the task is
11
+ # [+default?+] whether to add this task to new projects by default
12
+ class Task < BaseModel
13
+ include HappyMapper
14
+
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
+
24
+ def active?
25
+ !deactivated
26
+ end
27
+
28
+ alias_method :default?, :default
29
+ end
30
+ end
@@ -0,0 +1,34 @@
1
+ module Harvest
2
+ class TaskAssignment < BaseModel
3
+ include HappyMapper
4
+
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
+
13
+ def task=(task)
14
+ @task_id = task.to_i
15
+ end
16
+
17
+ def project=(project)
18
+ @project_id = project.to_i
19
+ end
20
+
21
+ def active?
22
+ !deactivated
23
+ end
24
+
25
+ def task_xml
26
+ builder = Builder::XmlMarkup.new
27
+ builder.task do |t|
28
+ t.id(task_id)
29
+ end
30
+ end
31
+
32
+ alias_method :billable?, :billable
33
+ end
34
+ end
@@ -0,0 +1,42 @@
1
+ module Harvest
2
+ class TimeEntry < BaseModel
3
+ include HappyMapper
4
+
5
+ tag 'day_entry'
6
+
7
+ element :id, Integer
8
+ element :client, String
9
+ element :project, String
10
+ element :task, String
11
+ element :hours, Float
12
+ element :notes, String
13
+
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
+
23
+ def spent_at=(date)
24
+ @spent_at = (String === date ? Time.parse(date) : date)
25
+ end
26
+
27
+ def to_xml
28
+ builder = Builder::XmlMarkup.new
29
+ builder.request do |r|
30
+ r.tag!('notes', notes) if notes
31
+ r.tag!('hours', hours) if hours
32
+ r.tag!('project_id', project_id) if project_id
33
+ r.tag!('task_id', task_id) if task_id
34
+ r.tag!('spent_at', spent_at) if spent_at
35
+ r.tag!('of_user', user_id) if user_id
36
+ end
37
+ end
38
+
39
+ alias_method :closed?, :closed
40
+ alias_method :billed?, :billed
41
+ end
42
+ end
@@ -0,0 +1,149 @@
1
+ module Harvest
2
+ # shamelessly ripped from Rails: http://github.com/rails/rails/blob/master/activesupport/lib/active_support/values/time_zone.rb
3
+ module Timezones
4
+ MAPPING = {
5
+ "pacific/midway" => "International Date Line West",
6
+ "pacific/midway" => "Midway Island",
7
+ "pacific/pago_pago" => "Samoa",
8
+ "pacific/honolulu" => "Hawaii",
9
+ "america/juneau" => "Alaska",
10
+ "america/los_angeles" => "Pacific Time (US & Canada)",
11
+ "america/tijuana" => "Tijuana",
12
+ "america/denver" => "Mountain Time (US & Canada)",
13
+ "america/phoenix" => "Arizona",
14
+ "america/chihuahua" => "Chihuahua",
15
+ "america/mazatlan" => "Mazatlan",
16
+ "america/chicago" => "Central Time (US & Canada)",
17
+ "america/regina" => "Saskatchewan",
18
+ "america/mexico_city" => "Guadalajara",
19
+ "america/mexico_city" => "Mexico City",
20
+ "america/monterrey" => "Monterrey",
21
+ "america/guatemala" => "Central America",
22
+ "america/new_york" => "Eastern Time (US & Canada)",
23
+ "america/indiana/indianapolis" => "Indiana (East)",
24
+ "america/bogota" => "Bogota",
25
+ "america/lima" => "Lima",
26
+ "america/lima" => "Quito",
27
+ "america/halifax" => "Atlantic Time (Canada)",
28
+ "america/caracas" => "Caracas",
29
+ "america/la_paz" => "La Paz",
30
+ "america/santiago" => "Santiago",
31
+ "america/st_johns" => "Newfoundland",
32
+ "america/sao_paulo" => "Brasilia",
33
+ "america/argentina/buenos_aires" => "Buenos Aires",
34
+ "america/argentina/san_juan" => "Georgetown",
35
+ "america/godthab" => "Greenland",
36
+ "atlantic/south_georgia" => "Mid-Atlantic",
37
+ "atlantic/azores" => "Azores",
38
+ "atlantic/cape_verde" => "Cape Verde Is.",
39
+ "europe/dublin" => "Dublin",
40
+ "europe/dublin" => "Edinburgh",
41
+ "europe/lisbon" => "Lisbon",
42
+ "europe/london" => "London",
43
+ "africa/casablanca" => "Casablanca",
44
+ "africa/monrovia" => "Monrovia",
45
+ "etc/utc" => "UTC",
46
+ "europe/belgrade" => "Belgrade",
47
+ "europe/bratislava" => "Bratislava",
48
+ "europe/budapest" => "Budapest",
49
+ "europe/ljubljana" => "Ljubljana",
50
+ "europe/prague" => "Prague",
51
+ "europe/sarajevo" => "Sarajevo",
52
+ "europe/skopje" => "Skopje",
53
+ "europe/warsaw" => "Warsaw",
54
+ "europe/zagreb" => "Zagreb",
55
+ "europe/brussels" => "Brussels",
56
+ "europe/copenhagen" => "Copenhagen",
57
+ "europe/madrid" => "Madrid",
58
+ "europe/paris" => "Paris",
59
+ "europe/amsterdam" => "Amsterdam",
60
+ "europe/berlin" => "Berlin",
61
+ "europe/berlin" => "Bern",
62
+ "europe/rome" => "Rome",
63
+ "europe/stockholm" => "Stockholm",
64
+ "europe/vienna" => "Vienna",
65
+ "africa/algiers" => "West Central Africa",
66
+ "europe/bucharest" => "Bucharest",
67
+ "africa/cairo" => "Cairo",
68
+ "europe/helsinki" => "Helsinki",
69
+ "europe/kiev" => "Kyev",
70
+ "europe/riga" => "Riga",
71
+ "europe/sofia" => "Sofia",
72
+ "europe/tallinn" => "Tallinn",
73
+ "europe/vilnius" => "Vilnius",
74
+ "europe/athens" => "Athens",
75
+ "europe/istanbul" => "Istanbul",
76
+ "europe/minsk" => "Minsk",
77
+ "asia/jerusalem" => "Jerusalem",
78
+ "africa/harare" => "Harare",
79
+ "africa/johannesburg" => "Pretoria",
80
+ "europe/moscow" => "Moscow",
81
+ "europe/moscow" => "St. Petersburg",
82
+ "europe/moscow" => "Volgograd",
83
+ "asia/kuwait" => "Kuwait",
84
+ "asia/riyadh" => "Riyadh",
85
+ "africa/nairobi" => "Nairobi",
86
+ "asia/baghdad" => "Baghdad",
87
+ "asia/tehran" => "Tehran",
88
+ "asia/muscat" => "Abu Dhabi",
89
+ "asia/muscat" => "Muscat",
90
+ "asia/baku" => "Baku",
91
+ "asia/tbilisi" => "Tbilisi",
92
+ "asia/yerevan" => "Yerevan",
93
+ "asia/kabul" => "Kabul",
94
+ "asia/yekaterinburg" => "Ekaterinburg",
95
+ "asia/karachi" => "Islamabad",
96
+ "asia/karachi" => "Karachi",
97
+ "asia/tashkent" => "Tashkent",
98
+ "asia/kolkata" => "Chennai",
99
+ "asia/kolkata" => "Kolkata",
100
+ "asia/kolkata" => "Mumbai",
101
+ "asia/kolkata" => "New Delhi",
102
+ "asia/katmandu" => "Kathmandu",
103
+ "asia/dhaka" => "Astana",
104
+ "asia/dhaka" => "Dhaka",
105
+ "asia/colombo" => "Sri Jayawardenepura",
106
+ "asia/almaty" => "Almaty",
107
+ "asia/novosibirsk" => "Novosibirsk",
108
+ "asia/rangoon" => "Rangoon",
109
+ "asia/bangkok" => "Bangkok",
110
+ "asia/bangkok" => "Hanoi",
111
+ "asia/jakarta" => "Jakarta",
112
+ "asia/krasnoyarsk" => "Krasnoyarsk",
113
+ "asia/shanghai" => "Beijing",
114
+ "asia/chongqing" => "Chongqing",
115
+ "asia/hong_kong" => "Hong Kong",
116
+ "asia/urumqi" => "Urumqi",
117
+ "asia/kuala_lumpur" => "Kuala Lumpur",
118
+ "asia/singapore" => "Singapore",
119
+ "asia/taipei" => "Taipei",
120
+ "australia/perth" => "Perth",
121
+ "asia/irkutsk" => "Irkutsk",
122
+ "asia/ulaanbaatar" => "Ulaan Bataar",
123
+ "asia/seoul" => "Seoul",
124
+ "asia/tokyo" => "Osaka",
125
+ "asia/tokyo" => "Sapporo",
126
+ "asia/tokyo" => "Tokyo",
127
+ "asia/yakutsk" => "Yakutsk",
128
+ "australia/darwin" => "Darwin",
129
+ "australia/adelaide" => "Adelaide",
130
+ "australia/melbourne" => "Canberra",
131
+ "australia/melbourne" => "Melbourne",
132
+ "australia/sydney" => "Sydney",
133
+ "australia/brisbane" => "Brisbane",
134
+ "australia/hobart" => "Hobart",
135
+ "asia/vladivostok" => "Vladivostok",
136
+ "pacific/guam" => "Guam",
137
+ "pacific/port_moresby" => "Port Moresby",
138
+ "asia/magadan" => "Magadan",
139
+ "asia/magadan" => "Solomon Is.",
140
+ "pacific/noumea" => "New Caledonia",
141
+ "pacific/fiji" => "Fiji",
142
+ "asia/kamchatka" => "Kamchatka",
143
+ "pacific/majuro" => "Marshall Is.",
144
+ "pacific/auckland" => "Auckland",
145
+ "pacific/auckland" => "Wellington",
146
+ "pacific/tongatapu" => "Nuku'alofa"
147
+ }
148
+ end
149
+ end
@@ -0,0 +1,66 @@
1
+ module Harvest
2
+
3
+ # The model that contains information about a task
4
+ #
5
+ # == Fields
6
+ # [+id+] (READONLY) the id of the user
7
+ # [+email+] the email of the user
8
+ # [+first_name+] the first name for the user
9
+ # [+last_name+] the last name for the user
10
+ # [+telephone+] the telephone for the user
11
+ # [+department] the department for the user
12
+ # [+password|password_confirmation+] the password for the user (only used on create.)
13
+ # [+has_access_to_all_future_projects+] whether the user should be added to future projects by default
14
+ # [+hourly_rate+] what the default hourly rate for the user is
15
+ # [+admin?+] whether the user is an admin
16
+ # [+contractor?+] whether the user is a contractor
17
+ # [+contractor?+] whether the user is a contractor
18
+ # [+timezone+] the timezone for the user.
19
+ class User < BaseModel
20
+ include HappyMapper
21
+
22
+ 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'
37
+
38
+ alias_method :active?, :active
39
+ alias_method :admin?, :admin
40
+ alias_method :contractor?, :contractor
41
+
42
+ # Sets the timezone for the user. This can be done in a variety of ways.
43
+ #
44
+ # == Examples
45
+ # user.timezone = :cst # the easiest way. CST, EST, MST, and PST are supported
46
+ #
47
+ # user.timezone = 'america/chicago' # a little more verbose
48
+ #
49
+ # user.timezone = 'Central Time (US & Canada)' # the most explicit way
50
+ def timezone=(timezone)
51
+ tz = timezone.to_s.downcase
52
+ case tz
53
+ when 'cst', 'cdt' then self.timezone = 'america/chicago'
54
+ when 'est', 'edt' then self.timezone = 'america/new_york'
55
+ when 'mst', 'mdt' then self.timezone = 'america/denver'
56
+ when 'pst', 'pdt' then self.timezone = 'america/los_angeles'
57
+ else
58
+ if Harvest::Timezones::MAPPING[tz]
59
+ @timezone = Harvest::Timezones::MAPPING[tz]
60
+ else
61
+ @timezone = timezone
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,34 @@
1
+ module Harvest
2
+ class UserAssignment < BaseModel
3
+ include HappyMapper
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'
12
+
13
+ def user=(user)
14
+ @user_id = user.to_i
15
+ end
16
+
17
+ def project=(project)
18
+ @project_id = project.to_i
19
+ end
20
+
21
+ def active?
22
+ !deactivated
23
+ end
24
+
25
+ def user_xml
26
+ builder = Builder::XmlMarkup.new
27
+ builder.user do |t|
28
+ t.id(user_id)
29
+ end
30
+ end
31
+
32
+ alias_method :project_manager?, :project_manager
33
+ end
34
+ end
data/lib/harvested.rb ADDED
@@ -0,0 +1,62 @@
1
+ require 'happymapper'
2
+ require 'httparty'
3
+ require 'base64'
4
+ require 'builder'
5
+ require 'delegate'
6
+
7
+ require 'harvest/credentials'
8
+ require 'harvest/errors'
9
+ require 'harvest/hardy_client'
10
+ require 'harvest/timezones'
11
+
12
+ require 'harvest/base'
13
+
14
+ %w(crud activatable).each {|a| require "harvest/behavior/#{a}"}
15
+ %w(base_model client contact project task user rate_limit_status task_assignment user_assignment expense_category expense time_entry).each {|a| require "harvest/#{a}"}
16
+ %w(base account clients contacts projects tasks users task_assignments user_assignments expense_categories expenses time reports).each {|a| require "harvest/api/#{a}"}
17
+
18
+ module Harvest
19
+ VERSION = "0.3.1".freeze
20
+
21
+ class << self
22
+
23
+ # Creates a standard client that will raise all errors it encounters
24
+ #
25
+ # == Options
26
+ # * +:ssl+ - Whether or not to use SSL when connecting to Harvest. This is dependent on whether your account supports it. Set to +true+ by default
27
+ # == Examples
28
+ # Harvest.client('mysubdomain', 'myusername', 'mypassword', :ssl => false)
29
+ #
30
+ # @return [Harvest::Base]
31
+ def client(subdomain, username, password, options = {})
32
+ Harvest::Base.new(subdomain, username, password, options)
33
+ end
34
+
35
+ # Creates a hardy client that will retry common HTTP errors it encounters and sleep() if it determines it is over your rate limit
36
+ #
37
+ # == Options
38
+ # * +:ssl+ - Whether or not to use SSL when connecting to Harvest. This is dependent on whether your account supports it. Set to +true+ by default
39
+ # * +:retry+ - How many times the hardy client should retry errors. Set to +5+ by default.
40
+ #
41
+ # == Examples
42
+ # Harvest.hardy_client('mysubdomain', 'myusername', 'mypassword', :ssl => true, :retry => 3)
43
+ #
44
+ # == Errors
45
+ # The hardy client will retry the following errors
46
+ # * Harvest::Unavailable
47
+ # * Harvest::InformHarvest
48
+ # * Net::HTTPError
49
+ # * Net::HTTPFatalError
50
+ # * Errno::ECONNRESET
51
+ #
52
+ # == Rate Limits
53
+ # The hardy client will make as many requests as it can until it detects it has gone over the rate limit. Then it will +sleep()+ for the how ever long it takes for the limit to reset. You can find more information about the Rate Limiting at http://www.getharvest.com/api
54
+ #
55
+ # @return [Harvest::HardyClient] a Harvest::Base wrapped in a Harvest::HardyClient
56
+ # @see Harvest::Base
57
+ def hardy_client(subdomain, username, password, options = {})
58
+ retries = options.delete(:retry)
59
+ Harvest::HardyClient.new(client(subdomain, username, password, options), (retries || 5))
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,9 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
2
+
3
+ describe Harvest::Base do
4
+ describe "username/password errors" do
5
+ it "should raise error if missing a credential" do
6
+ lambda { Harvest::Base.new("subdomain", nil, "secure") }.should raise_error(Harvest::InvalidCredentials)
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,22 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
2
+
3
+ describe Harvest::Credentials do
4
+ describe "#valid?" do
5
+ it "should return true if domain, username, and password is filled out" do
6
+ Harvest::Credentials.new("some-domain", "username", "password").should be_valid
7
+ end
8
+
9
+ it "should return false if either domain, username, or password is nil" do
10
+ Harvest::Credentials.new("some-domain", "username", nil).should_not be_valid
11
+ Harvest::Credentials.new("some-domain", nil, "password").should_not be_valid
12
+ Harvest::Credentials.new(nil, "username", "password").should_not be_valid
13
+ Harvest::Credentials.new(nil, nil, nil).should_not be_valid
14
+ end
15
+ end
16
+
17
+ describe "#basic_auth" do
18
+ it "should base64 encode the credentials" do
19
+ Harvest::Credentials.new("some-domain", "username", "password").basic_auth.should == "dXNlcm5hbWU6cGFzc3dvcmQ="
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,15 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
2
+
3
+ describe Harvest::Expense do
4
+ describe "#spent_at" do
5
+ it "should parse strings" do
6
+ expense = Harvest::Expense.new(:spent_at => "12/01/2009")
7
+ expense.spent_at.should == Time.parse("12/01/2009")
8
+ end
9
+
10
+ it "should accept times" do
11
+ expense = Harvest::Expense.new(:spent_at => Time.parse("12/01/2009"))
12
+ expense.spent_at.should == Time.parse("12/01/2009")
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,10 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
2
+
3
+ describe Harvest::TaskAssignment do
4
+ describe "#task_xml" do
5
+ it "should generate the xml for existing tasks" do
6
+ assignment = Harvest::TaskAssignment.new(:task => mock(:task, :to_i => 3))
7
+ assignment.task_xml.should == '<task><id>3</id></task>'
8
+ end
9
+ end
10
+ end