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,154 @@
1
+ module Forecast
2
+ module Model
3
+ def self.included(base)
4
+ base.send :include, InstanceMethods
5
+ base.send :extend, ClassMethods
6
+ end
7
+
8
+ module InstanceMethods
9
+ def to_json(*args)
10
+ as_json(*args).to_json(*args)
11
+ end
12
+
13
+ def as_json(args = {})
14
+ inner_json = self.to_hash.stringify_keys
15
+ inner_json.delete("cache_version")
16
+ if self.class.skip_json_root?
17
+ inner_json
18
+ else
19
+ { self.class.json_root => inner_json }
20
+ end
21
+ end
22
+
23
+ def to_i; id; end
24
+
25
+ def ==(other)
26
+ other.kind_of?(self.class) && id == other.id
27
+ end
28
+
29
+ def impersonated_user_id
30
+ if respond_to?(:of_user) && respond_to?(:user_id)
31
+ of_user || user_id
32
+ elsif !respond_to?(:of_user) && respond_to?(:user_id)
33
+ user_id
34
+ elsif respond_to?(:of_user)
35
+ of_user
36
+ end
37
+ end
38
+
39
+ def json_root
40
+ self.class.json_root
41
+ end
42
+ end
43
+
44
+ module ClassMethods
45
+ # This sets the API path so the API collections can use them in an agnostic way
46
+ # @return [void]
47
+ def api_path(path = nil)
48
+ @_api_path ||= path
49
+ end
50
+
51
+ def skip_json_root(skip = nil)
52
+ @_skip_json_root ||= skip
53
+ end
54
+
55
+ def skip_json_root?
56
+ @_skip_json_root == true
57
+ end
58
+
59
+ def parse(json)
60
+ #
61
+ # old gem
62
+ # parsed = String === json ? JSON.parse(json) : json
63
+ # Array.wrap(parsed).map {|attrs| skip_json_root? ? new(attrs) : new(attrs[json_root])}
64
+ #
65
+
66
+ # parses json into a ruby hash
67
+ # {'projects' => [{id: 123}, {id: 456}]}
68
+ parsed = String === json ? JSON.parse(json) : json
69
+
70
+
71
+ # @todo @weston
72
+ # total hack
73
+ if parsed.keys == [json_root]
74
+ # single object
75
+ # before # {"project"=>{"id"=>123"}}
76
+ # after # {"id"=>123}
77
+ object_hash = parsed[json_root]
78
+ new(object_hash)
79
+ else
80
+ # list of objects
81
+ # before # {'projects' => [{id: 123}, {id: 456}]}
82
+ # after # [{id: 123}, {id: 456}]
83
+ object_hashes = parsed.to_a[0][1]
84
+ return object_hashes.map {|attrs| new(attrs)}
85
+ end
86
+ end
87
+
88
+ def json_root
89
+ Forecast::Model::Utility.underscore(
90
+ Forecast::Model::Utility.demodulize(to_s))
91
+ end
92
+
93
+ def wrap(model_or_attrs)
94
+ case model_or_attrs
95
+ when Hashie::Mash
96
+ model_or_attrs
97
+ when Hash
98
+ new(model_or_attrs)
99
+ else
100
+ model_or_attrs
101
+ end
102
+ end
103
+
104
+ def delegate_methods(options)
105
+ raise "no methods given" if options.empty?
106
+ options.each do |source, dest|
107
+ class_eval <<-EOV
108
+ def #{source}
109
+ #{dest}
110
+ end
111
+ EOV
112
+ end
113
+ end
114
+ end
115
+
116
+ module Utility
117
+ class << self
118
+
119
+ # Both methods are shamelessly ripped from https://github.com/rails/rails/blob/master/activesupport/lib/active_support/inflector/inflections.rb
120
+
121
+ # Removes the module part from the expression in the string.
122
+ #
123
+ # Examples:
124
+ # "ActiveRecord::CoreExtensions::String::Inflections".demodulize # => "Inflections"
125
+ # "Inflections".demodulize
126
+ def demodulize(class_name_in_module)
127
+ class_name_in_module.to_s.gsub(/^.*::/, '')
128
+ end
129
+
130
+ # Makes an underscored, lowercase form from the expression in the string.
131
+ #
132
+ # Changes '::' to '/' to convert namespaces to paths.
133
+ #
134
+ # Examples:
135
+ # "ActiveRecord".underscore # => "active_record"
136
+ # "ActiveRecord::Errors".underscore # => active_record/errors
137
+ #
138
+ # As a rule of thumb you can think of +underscore+ as the inverse of +camelize+,
139
+ # though there are cases where that does not hold:
140
+ #
141
+ # "SSLError".underscore.camelize # => "SslError"
142
+ def underscore(camel_cased_word)
143
+ word = camel_cased_word.to_s.dup
144
+ word.gsub!(/::/, '/')
145
+ word.gsub!(/([A-Z]+)([A-Z][a-z])/,'\1_\2')
146
+ word.gsub!(/([a-z\d])([A-Z])/,'\1_\2')
147
+ word.tr!("-", "_")
148
+ word.downcase!
149
+ word
150
+ end
151
+ end
152
+ end
153
+ end
154
+ end
@@ -0,0 +1,37 @@
1
+ module Forecast
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 < Hashie::Mash
26
+ include Forecast::Model
27
+
28
+ api_path '/projects'
29
+
30
+ def as_json(args = {})
31
+ super(args).tap do |json|
32
+ # json[json_root].delete("hint_earliest_record_at")
33
+ # json[json_root].delete("hint_latest_record_at")
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,23 @@
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 < Hashie::Mash
12
+ # include Harvest::Model
13
+
14
+ # skip_json_root true
15
+
16
+ # # Returns true if the user is over their rate limit
17
+ # # @return [Boolean]
18
+ # # @see http://www.getharvest.com/api
19
+ # def over_limit?
20
+ # count > max_calls
21
+ # end
22
+ # end
23
+ # end
@@ -0,0 +1,22 @@
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 < Hashie::Mash
13
+ # include Harvest::Model
14
+
15
+ # api_path '/tasks'
16
+ # delegate_methods :default? => :is_default
17
+
18
+ # def active?
19
+ # !deactivated
20
+ # end
21
+ # end
22
+ # end
@@ -0,0 +1,25 @@
1
+ # module Harvest
2
+ # class TimeEntry < Hashie::Mash
3
+ # include Harvest::Model
4
+
5
+ # skip_json_root true
6
+ # delegate_methods(:closed? => :is_closed,
7
+ # :billed? => :is_billed)
8
+
9
+ # def initialize(args = {}, _ = nil)
10
+ # args = args.to_hash.stringify_keys
11
+ # self.spent_at = args.delete("spent_at") if args["spent_at"]
12
+ # super
13
+ # end
14
+
15
+ # def spent_at=(date)
16
+ # self["spent_at"] = Date.parse(date.to_s)
17
+ # end
18
+
19
+ # def as_json(args = {})
20
+ # super(args).to_hash.stringify_keys.tap do |hash|
21
+ # hash.update("spent_at" => (spent_at.nil? ? nil : spent_at.xmlschema))
22
+ # end
23
+ # end
24
+ # end
25
+ # end
@@ -0,0 +1,130 @@
1
+ module Forecast
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" => "Midway Island",
6
+ "pacific/pago_pago" => "Samoa",
7
+ "pacific/honolulu" => "Hawaii",
8
+ "america/juneau" => "Alaska",
9
+ "america/los_angeles" => "Pacific Time (US & Canada)",
10
+ "america/tijuana" => "Tijuana",
11
+ "america/denver" => "Mountain Time (US & Canada)",
12
+ "america/phoenix" => "Arizona",
13
+ "america/chihuahua" => "Chihuahua",
14
+ "america/mazatlan" => "Mazatlan",
15
+ "america/chicago" => "Central Time (US & Canada)",
16
+ "america/regina" => "Saskatchewan",
17
+ "america/mexico_city" => "Mexico City",
18
+ "america/monterrey" => "Monterrey",
19
+ "america/guatemala" => "Central America",
20
+ "america/new_york" => "Eastern Time (US & Canada)",
21
+ "america/indiana/indianapolis" => "Indiana (East)",
22
+ "america/bogota" => "Bogota",
23
+ "america/lima" => "Lima",
24
+ "america/halifax" => "Atlantic Time (Canada)",
25
+ "america/caracas" => "Caracas",
26
+ "america/la_paz" => "La Paz",
27
+ "america/santiago" => "Santiago",
28
+ "america/st_johns" => "Newfoundland",
29
+ "america/sao_paulo" => "Brasilia",
30
+ "america/argentina/buenos_aires" => "Buenos Aires",
31
+ "america/argentina/san_juan" => "Georgetown",
32
+ "america/godthab" => "Greenland",
33
+ "atlantic/south_georgia" => "Mid-Atlantic",
34
+ "atlantic/azores" => "Azores",
35
+ "atlantic/cape_verde" => "Cape Verde Is.",
36
+ "europe/dublin" => "Dublin",
37
+ "europe/lisbon" => "Lisbon",
38
+ "europe/london" => "London",
39
+ "africa/casablanca" => "Casablanca",
40
+ "africa/monrovia" => "Monrovia",
41
+ "etc/utc" => "UTC",
42
+ "europe/belgrade" => "Belgrade",
43
+ "europe/bratislava" => "Bratislava",
44
+ "europe/budapest" => "Budapest",
45
+ "europe/ljubljana" => "Ljubljana",
46
+ "europe/prague" => "Prague",
47
+ "europe/sarajevo" => "Sarajevo",
48
+ "europe/skopje" => "Skopje",
49
+ "europe/warsaw" => "Warsaw",
50
+ "europe/zagreb" => "Zagreb",
51
+ "europe/brussels" => "Brussels",
52
+ "europe/copenhagen" => "Copenhagen",
53
+ "europe/madrid" => "Madrid",
54
+ "europe/paris" => "Paris",
55
+ "europe/amsterdam" => "Amsterdam",
56
+ "europe/berlin" => "Berlin",
57
+ "europe/rome" => "Rome",
58
+ "europe/stockholm" => "Stockholm",
59
+ "europe/vienna" => "Vienna",
60
+ "africa/algiers" => "West Central Africa",
61
+ "europe/bucharest" => "Bucharest",
62
+ "africa/cairo" => "Cairo",
63
+ "europe/helsinki" => "Helsinki",
64
+ "europe/kiev" => "Kyev",
65
+ "europe/riga" => "Riga",
66
+ "europe/sofia" => "Sofia",
67
+ "europe/tallinn" => "Tallinn",
68
+ "europe/vilnius" => "Vilnius",
69
+ "europe/athens" => "Athens",
70
+ "europe/istanbul" => "Istanbul",
71
+ "europe/minsk" => "Minsk",
72
+ "asia/jerusalem" => "Jerusalem",
73
+ "africa/harare" => "Harare",
74
+ "africa/johannesburg" => "Pretoria",
75
+ "europe/moscow" => "Moscow",
76
+ "asia/kuwait" => "Kuwait",
77
+ "asia/riyadh" => "Riyadh",
78
+ "africa/nairobi" => "Nairobi",
79
+ "asia/baghdad" => "Baghdad",
80
+ "asia/tehran" => "Tehran",
81
+ "asia/muscat" => "Muscat",
82
+ "asia/baku" => "Baku",
83
+ "asia/tbilisi" => "Tbilisi",
84
+ "asia/yerevan" => "Yerevan",
85
+ "asia/kabul" => "Kabul",
86
+ "asia/yekaterinburg" => "Ekaterinburg",
87
+ "asia/karachi" => "Karachi",
88
+ "asia/tashkent" => "Tashkent",
89
+ "asia/kolkata" => "Kolkata",
90
+ "asia/katmandu" => "Kathmandu",
91
+ "asia/dhaka" => "Dhaka",
92
+ "asia/colombo" => "Sri Jayawardenepura",
93
+ "asia/almaty" => "Almaty",
94
+ "asia/novosibirsk" => "Novosibirsk",
95
+ "asia/rangoon" => "Rangoon",
96
+ "asia/bangkok" => "Bangkok",
97
+ "asia/jakarta" => "Jakarta",
98
+ "asia/krasnoyarsk" => "Krasnoyarsk",
99
+ "asia/shanghai" => "Beijing",
100
+ "asia/chongqing" => "Chongqing",
101
+ "asia/hong_kong" => "Hong Kong",
102
+ "asia/urumqi" => "Urumqi",
103
+ "asia/kuala_lumpur" => "Kuala Lumpur",
104
+ "asia/singapore" => "Singapore",
105
+ "asia/taipei" => "Taipei",
106
+ "australia/perth" => "Perth",
107
+ "asia/irkutsk" => "Irkutsk",
108
+ "asia/ulaanbaatar" => "Ulaan Bataar",
109
+ "asia/seoul" => "Seoul",
110
+ "asia/tokyo" => "Tokyo",
111
+ "asia/yakutsk" => "Yakutsk",
112
+ "australia/darwin" => "Darwin",
113
+ "australia/adelaide" => "Adelaide",
114
+ "australia/melbourne" => "Melbourne",
115
+ "australia/sydney" => "Sydney",
116
+ "australia/brisbane" => "Brisbane",
117
+ "australia/hobart" => "Hobart",
118
+ "asia/vladivostok" => "Vladivostok",
119
+ "pacific/guam" => "Guam",
120
+ "pacific/port_moresby" => "Port Moresby",
121
+ "asia/magadan" => "Magadan",
122
+ "pacific/noumea" => "New Caledonia",
123
+ "pacific/fiji" => "Fiji",
124
+ "asia/kamchatka" => "Kamchatka",
125
+ "pacific/majuro" => "Marshall Is.",
126
+ "pacific/auckland" => "Auckland",
127
+ "pacific/tongatapu" => "Nuku'alofa"
128
+ }
129
+ end
130
+ end
@@ -0,0 +1,38 @@
1
+ # module Harvest
2
+
3
+ # # The model for project-tasks combinations that can be added to the timesheet
4
+ # #
5
+ # # == Fields
6
+ # # [+id+] the id of the project
7
+ # # [+name+] the name of the project
8
+ # # [+client+] the name of the client of the project
9
+ # # [+client_id+] the client id of the project
10
+ # # [+tasks+] trackable tasks for the project
11
+ # class TrackableProject < Hashie::Mash
12
+ # include Harvest::Model
13
+
14
+ # skip_json_root true
15
+
16
+ # def initialize(args = {}, _ = nil)
17
+ # args = args.to_hash.stringify_keys
18
+ # self.tasks = args.delete("tasks") if args["tasks"]
19
+ # super
20
+ # end
21
+
22
+ # def tasks=(tasks)
23
+ # self["tasks"] = Task.parse(tasks)
24
+ # end
25
+
26
+ # # The model for trackable tasks
27
+ # #
28
+ # # == Fields
29
+ # # [+id+] the id of the task
30
+ # # [+name+] the name of the task
31
+ # # [+billable+] whether the task is billable by default
32
+ # class Task < Hashie::Mash
33
+ # include Harvest::Model
34
+
35
+ # skip_json_root true
36
+ # end
37
+ # end
38
+ # end
@@ -0,0 +1,30 @@
1
+ # module Harvest
2
+ # class UserAssignment < Hashie::Mash
3
+ # include Harvest::Model
4
+
5
+ # delegate_methods :project_manager? => :is_project_manager
6
+
7
+ # def initialize(args = {}, _ = nil)
8
+ # args = args.to_hash.stringify_keys
9
+ # self.user = args.delete("user") if args["user"]
10
+ # self.project = args.delete("project") if args["project"]
11
+ # super
12
+ # end
13
+
14
+ # def user=(user)
15
+ # self["user_id"] = user.to_i
16
+ # end
17
+
18
+ # def project=(project)
19
+ # self["project_id"] = project.to_i
20
+ # end
21
+
22
+ # def active?
23
+ # !deactivated
24
+ # end
25
+
26
+ # def user_as_json
27
+ # {"user" => {"id" => user_id}}
28
+ # end
29
+ # end
30
+ # end
@@ -0,0 +1,3 @@
1
+ module Forecast
2
+ VERSION = "0.0.1"
3
+ end
data/lib/forecasted.rb ADDED
@@ -0,0 +1,87 @@
1
+ require 'httparty'
2
+ require 'base64'
3
+ require 'delegate'
4
+ require 'hashie'
5
+ require 'json'
6
+ require 'time'
7
+ require 'csv'
8
+
9
+ require 'ext/array'
10
+ require 'ext/hash'
11
+ require 'ext/date'
12
+ require 'ext/time'
13
+
14
+ require 'forecast/version'
15
+ require 'forecast/credentials'
16
+ require 'forecast/errors'
17
+ require 'forecast/hardy_client'
18
+ require 'forecast/timezones'
19
+
20
+ require 'forecast/base'
21
+
22
+ %w(crud activatable).each {|a| require "forecast/behavior/#{a}"}
23
+
24
+ %w(model project assignment aggregate).each {|a| require "forecast/#{a}"}
25
+ %w(base projects assignments aggregates).each {|a| require "forecast/api/#{a}"}
26
+
27
+ module Forecast
28
+ class << self
29
+
30
+ # Creates a standard client that will raise all errors it encounters
31
+ #
32
+ # == Options
33
+ # * Basic Authentication
34
+ # * +:subdomain+ - Your HarvestForecast subdomain
35
+ # * +:username+ - Your HarvestForecast username
36
+ # * +:password+ - Your HarvestForecast password
37
+ # * OAuth
38
+ # * +:access_token+ - An OAuth 2.0 access token
39
+ #
40
+ # == Examples
41
+ # Forecast.client(subdomain: 'mysubdomain', username: 'myusername', password: 'mypassword')
42
+ # Forecast.client(access_token: 'myaccesstoken')
43
+ #
44
+ # @return [Forecast::Base]
45
+ # def client(subdomain: nil, username: nil, password: nil, access_token: nil)
46
+ def client(ops={})
47
+ # Forecast::Base.new(subdomain: subdomain, username: username, password: password, access_token: access_token)
48
+ Forecast::Base.new(ops)
49
+ end
50
+
51
+ # Creates a hardy client that will retry common HTTP errors it encounters and sleep() if it determines it is over your rate limit
52
+ #
53
+ # == Options
54
+ # * Basic Authentication
55
+ # * +:subdomain+ - Your HarvestForecast subdomain
56
+ # * +:username+ - Your HarvestForecast username
57
+ # * +:password+ - Your HarvestForecast password
58
+ # * OAuth
59
+ # * +:access_token+ - An OAuth 2.0 access token
60
+ #
61
+ # * +:retry+ - How many times the hardy client should retry errors. Set to +5+ by default.
62
+ #
63
+ # == Examples
64
+ # Forecast.hardy_client(subdomain: 'mysubdomain', username: 'myusername', password: 'mypassword', retry: 3)
65
+ #
66
+ # Forecast.hardy_client(access_token: 'myaccesstoken', retries: 3)
67
+ #
68
+ # == Errors
69
+ # The hardy client will retry the following errors
70
+ # * Forecast::Unavailable
71
+ # * Forecast::InformHarvest
72
+ # * Net::HTTPError
73
+ # * Net::HTTPFatalError
74
+ # * Errno::ECONNRESET
75
+ #
76
+ # == Rate Limits
77
+ # 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.getforecast.com/api
78
+ #
79
+ # @return [Forecast::HardyClient] a Harvest::Base wrapped in a Harvest::HardyClient
80
+ # @see Forecast::Base
81
+ # def hardy_client(subdomain: nil, username: nil, password: nil, access_token: nil, retries: 5)
82
+ def hardy_client(ops={}, retries=5)
83
+ # Forecast::HardyClient.new(client(subdomain: subdomain, username: username, password: password, access_token: access_token), retries)
84
+ Forecast::HardyClient.new(client(ops), retries)
85
+ end
86
+ end
87
+ end
data/spec/factories.rb ADDED
@@ -0,0 +1,17 @@
1
+ FactoryGirl.define do
2
+ sequence :name do |n|
3
+ "Joe's Steam Cleaning #{n}"
4
+ end
5
+
6
+ sequence :description do |n|
7
+ "Item #{n}"
8
+ end
9
+
10
+ sequence :project_name do |n|
11
+ "Joe's Steam Cleaning Project #{n}"
12
+ end
13
+
14
+ factory :project, class: Forecast::Project do
15
+ name { generate(:project_name) }
16
+ end
17
+ end
@@ -0,0 +1,11 @@
1
+ require 'spec_helper'
2
+
3
+ describe Forecast::Base do
4
+ describe "forecast_account_id/access_token errors" do
5
+ it "should raise error if missing a credential" do
6
+ expect {
7
+ Forecast::Base.new({forecast_account_id: 123})
8
+ }.to raise_error(/must provide/i)
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,64 @@
1
+ require 'spec_helper'
2
+
3
+ describe 'forecast aggregate' do
4
+
5
+ describe 'future_scheduled_hours_after' do
6
+ it 'works' do
7
+ cassette('aggregate-future_scheduled_hours_after') do
8
+ x = forecast.aggregates.future_scheduled_hours_after("2016-11-29")
9
+
10
+ expect(x.class).to be(Array)
11
+ expect(x.first.class).to be(Forecast::Aggregate)
12
+ expect(x.size > 1).to eq(true)
13
+ end
14
+ end
15
+
16
+ it 'query by project_id' do
17
+ cassette('aggregate-future_scheduled_hours-query-project_id') do
18
+ x = forecast.aggregates.future_scheduled_hours_after("2016-11-29", {project_id: mm_forecast_project_id})
19
+
20
+ expect(x.class).to be(Array)
21
+ expect(x.first.class).to be(Forecast::Aggregate)
22
+
23
+ expect_belong_to_one_project(x)
24
+ end
25
+ end
26
+ end
27
+
28
+ describe 'remaining_budgeted_hours' do
29
+ it 'works' do
30
+ cassette('aggregate-remaining_budgeted_hours') do
31
+ x = forecast.aggregates.remaining_budgeted_hours
32
+
33
+ expect(x.class).to be(Array)
34
+ expect(x.first.class).to be(Forecast::Aggregate)
35
+
36
+ expect_belong_to_many_projects(x)
37
+ end
38
+ end
39
+
40
+ it 'query by (forecast) project_id' do
41
+ cassette('aggregate-remaining_budgeted_hours') do
42
+ x = forecast.aggregates.remaining_budgeted_hours({project_id: mm_forecast_project_id})
43
+
44
+ expect(x.class).to be(Array)
45
+ expect(x.first.class).to be(Forecast::Aggregate)
46
+
47
+ expect_belong_to_one_project(x)
48
+ end
49
+ end
50
+ end
51
+
52
+ def expect_belong_to_one_project(x)
53
+ project_ids = x.collect(&:project_id)
54
+ uniq_project_ids = project_ids.uniq
55
+ expect(uniq_project_ids.size).to eq(1)
56
+ end
57
+
58
+ def expect_belong_to_many_projects(x)
59
+ project_ids = x.collect(&:project_id)
60
+ uniq_project_ids = project_ids.uniq
61
+ expect(uniq_project_ids.size > 1).to eq(true)
62
+ end
63
+
64
+ end