forecasted 0.0.1

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 (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