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,131 @@
1
+ require 'spec_helper'
2
+
3
+ describe 'forecast assignments' do
4
+
5
+ describe 'all' do
6
+ it 'works' do
7
+ cassette('assignments-all') do
8
+ assignments = forecast.assignments.all
9
+
10
+ expect(assignments.class).to be(Array)
11
+ expect(assignments.first.class).to be(Forecast::Assignment)
12
+ expect(assignments.size > 1).to eq(true)
13
+ end
14
+ end
15
+
16
+ it 'query by start_date' do
17
+ cassette('assignments-query-start_date') do
18
+ assignments = forecast.assignments.all({start_date: '2016-01-01'})
19
+
20
+ expect(assignments.class).to be(Array)
21
+ expect(assignments.first.class).to be(Forecast::Assignment)
22
+ expect(assignments.size > 1).to eq(true)
23
+ end
24
+ end
25
+
26
+ it 'query by end_date' do
27
+ cassette('assignments-all-query-end_date') do
28
+ assignments = forecast.assignments.all({end_date: '2017-01-01'})
29
+
30
+ expect(assignments.class).to be(Array)
31
+ expect(assignments.first.class).to be(Forecast::Assignment)
32
+ expect(assignments.size > 1).to eq(true)
33
+ end
34
+ end
35
+
36
+ it 'query by start_and end_date' do
37
+ cassette('assignments-all-query-start_date-and-end_date') do
38
+ assignments = forecast.assignments.all({start_date: '2016-01-01', end_date: '2017-01-01'})
39
+
40
+ expect(assignments.class).to be(Array)
41
+ expect(assignments.first.class).to be(Forecast::Assignment)
42
+ expect(assignments.size > 1).to eq(true)
43
+ end
44
+ end
45
+
46
+ it 'query by status' do
47
+ cassette('assignments-all-query-status') do
48
+ assignments = forecast.assignments.all({state: 'active'})
49
+
50
+ expect(assignments.class).to be(Array)
51
+ expect(assignments.first.class).to be(Forecast::Assignment)
52
+ expect(assignments.size > 1).to eq(true)
53
+ end
54
+ end
55
+
56
+ it 'query by project_id' do
57
+ cassette('assignments-all-query-project_id') do
58
+ x = forecast.assignments.all({project_id: mm_forecast_project_id})
59
+
60
+ expect(x.class).to be(Array)
61
+ expect(x.first.class).to be(Forecast::Assignment)
62
+ expect(x.size > 1).to eq(true)
63
+
64
+ expect_belong_to_one_project(x)
65
+ end
66
+ end
67
+ end
68
+
69
+ describe 'find' do
70
+ it 'works' do
71
+ cassette('assignments-find') do
72
+ assignment_id = forecast.assignments.all.first.id
73
+ assignment = forecast.assignments.find(assignment_id)
74
+
75
+ expect(assignment.class).to be(Forecast::Assignment)
76
+ expect(assignment.id > 1).to eq(true)
77
+ end
78
+ end
79
+ end
80
+
81
+ describe 'by_project' do
82
+ it 'works' do
83
+ cassette('assignments-by_project') do
84
+ query = {
85
+ start_date: "2016-01-01",
86
+ end_date: "2018-01-01",
87
+ }
88
+
89
+ x = forecast.assignments.by_project(mm_forecast_project_id, query)
90
+
91
+ expect(x.class).to be(Array)
92
+ expect(x.first.class).to be(Forecast::Assignment)
93
+ expect(x.size > 1).to eq(true)
94
+
95
+ expect_belong_to_one_project(x)
96
+ end
97
+ end
98
+ end
99
+
100
+ describe 'last_by_project' do
101
+ it 'works' do
102
+ cassette('assignments-last_by_project') do
103
+ x = forecast.assignments.last_by_project(mm_forecast_project_id)
104
+
105
+ expect(x.class).to be(Forecast::Assignment)
106
+ expect(x.project_id).to eq(mm_forecast_project_id)
107
+
108
+ # manually checked it
109
+ expect(x.end_date).to eq("2016-12-01")
110
+ end
111
+ end
112
+ end
113
+
114
+ describe 'sum_allocation_seconds' do
115
+ it 'works' do
116
+ cassette('assignments-sum_allocation_seconds') do
117
+ query = {project_id: mm_forecast_project_id}
118
+
119
+ x = forecast.assignments.sum_allocation_seconds(query)
120
+
121
+ expect(x).to eq(62.0 * 3600)
122
+ end
123
+ end
124
+ end
125
+
126
+ def expect_belong_to_one_project(x)
127
+ project_ids = x.collect(&:project_id)
128
+ uniq_project_ids = project_ids.uniq
129
+ expect(uniq_project_ids.size).to eq(1)
130
+ end
131
+ end
@@ -0,0 +1,22 @@
1
+ require 'spec_helper'
2
+
3
+ describe 'forecast errors' do
4
+ before { WebMock.disable_net_connect! }
5
+
6
+ it "wraps errors" do
7
+ stub_request(:get, /\/projects/).to_return({:status => ['400', 'Bad Request']}, {:body => "[]", :status => 200})
8
+ expect { forecast.projects.all }.to raise_error(Forecast::BadRequest)
9
+
10
+ stub_request(:get, /\/projects/).to_return({:status => ['404', 'Not Found']}, {:body => "[]", :status => 200})
11
+ expect { forecast.projects.all }.to raise_error(Forecast::NotFound)
12
+
13
+ stub_request(:get, /\/projects/).to_return({:status => ['500', 'Server Error']}, {:body => "[]", :status => 200})
14
+ expect { forecast.projects.all }.to raise_error(Forecast::ServerError)
15
+
16
+ stub_request(:get, /\/projects/).to_return({:status => ['502', 'Bad Gateway']}, {:body => "[]", :status => 200})
17
+ expect { forecast.projects.all }.to raise_error(Forecast::Unavailable)
18
+
19
+ stub_request(:get, /\/projects/).to_return({:status => ['503', 'Rate Limited']}, {:body => "[]", :status => 200})
20
+ expect { forecast.projects.all }.to raise_error(Forecast::RateLimited)
21
+ end
22
+ end
@@ -0,0 +1,33 @@
1
+ require 'spec_helper'
2
+
3
+ describe 'forecat hardy client' do
4
+ before do
5
+ WebMock.disable_net_connect!
6
+ @time = Time.now
7
+ end
8
+
9
+ # it "waits the alloted time out when over rate limit" do
10
+ # stub_request(:get, /\/clients/).to_return({:status => ['503', 'Rate Limited'], :headers => {"Retry-After" => "5"}}, {:body => "[]", :status => 200})
11
+ # hardy_harvest.clients.all
12
+ # Time.now.should be_within(10).of(@time)
13
+ # end
14
+
15
+ # it "waits a default time when over the rate limit" do
16
+ # stub_request(:get, /\/clients/).to_return({:status => ['503', 'Rate Limited']}, {:body => "[]", :status => 200})
17
+ # hardy_harvest.clients.all
18
+ # Time.now.should be_within(20).of(@time)
19
+ # end
20
+
21
+ # it "retries after known errors" do
22
+ # stub_request(:get, /\/rate_limit_status/).to_return({:body => '{"lockout_seconds":2,"last_access_at":"2011-06-18T17:13:22+00:00","max_calls":100,"count":1,"timeframe_limit":15}'})
23
+
24
+ # stub_request(:get, /\/clients/).to_return({:status => ['502', 'Bad Gateway']}).times(2).then.to_return({:body => "[]", :status => 200})
25
+ # hardy_harvest.clients.all.should == []
26
+
27
+ # stub_request(:get, /\/clients/).to_raise(Net::HTTPError.new("custom error", "")).times(2).then.to_return({:body => "[]", :status => 200})
28
+ # hardy_harvest.clients.all.should == []
29
+
30
+ # stub_request(:get, /\/clients/).to_return({:status => ['502', 'Bad Gateway']}).times(6).then.to_return({:body => "[]", :status => 200})
31
+ # expect { hardy_harvest.clients.all }.to raise_error(Harvest::Unavailable)
32
+ # end
33
+ end
@@ -0,0 +1,82 @@
1
+ require 'spec_helper'
2
+
3
+ describe 'forecast milestones' do
4
+ # it 'allows adding, updating and removing tasks' do
5
+ # cassette('tasks') do
6
+ # task = harvest.tasks.create(
7
+ # "name" => "A crud task",
8
+ # "billable_by_default" => true,
9
+ # "default_hourly_rate" => 120
10
+ # )
11
+ # task.default_hourly_rate.should == 120.0
12
+
13
+ # task.default_hourly_rate = 140
14
+ # task = harvest.tasks.update(task)
15
+ # task.default_hourly_rate.should == 140.0
16
+
17
+ # harvest.tasks.delete(task)
18
+ # harvest.tasks.all.select {|t| t.name == "A crud task"}.should == []
19
+ # end
20
+ # end
21
+
22
+ # context "task assignments" do
23
+ # it "allows adding, updating, and removing tasks from projects" do
24
+ # cassette('tasks2') do
25
+ # client = harvest.clients.create(FactoryGirl.attributes_for(:client))
26
+
27
+ # project = harvest.projects.create(
28
+ # "name" => "Test Project2",
29
+ # "active" => true,
30
+ # "notes" => "project to test the api",
31
+ # "client_id" => client.id
32
+ # )
33
+
34
+ # task1 = harvest.tasks.create(
35
+ # "name" => "A task for joe",
36
+ # "billable_by_default" => true,
37
+ # "default_hourly_rate" => 120
38
+ # )
39
+
40
+ # # need to keep at least one task on the project
41
+ # task2 = harvest.tasks.create(
42
+ # "name" => "A task for joe2",
43
+ # "billable_by_default" => true,
44
+ # "default_hourly_rate" => 100
45
+ # )
46
+
47
+ # harvest.task_assignments.create("project" => project, "task" => task1)
48
+ # harvest.task_assignments.create("project" => project, "task" => task2)
49
+
50
+ # all_assignments = harvest.task_assignments.all(project)
51
+ # assignment1 = all_assignments.detect {|a| a.task_id == task1.id }
52
+ # assignment2 = all_assignments.detect {|a| a.task_id == task2.id }
53
+
54
+ # assignment1.hourly_rate = 100
55
+ # assignment1 = harvest.task_assignments.update(assignment1)
56
+ # assignment1.hourly_rate.should == 100.0
57
+
58
+ # harvest.task_assignments.delete(assignment1)
59
+ # all_assignments = harvest.task_assignments.all(project)
60
+ # all_assignments.size.should == 1
61
+ # end
62
+ # end
63
+
64
+ # it "allows creating and assigning the task at the same time" do
65
+ # cassette('tasks3') do
66
+ # client = harvest.clients.create(FactoryGirl.attributes_for(:client))
67
+
68
+ # project = harvest.projects.create(
69
+ # "name" => "Test Project3",
70
+ # "active" => true,
71
+ # "notes" => "project to test the api",
72
+ # "client_id" => client.id
73
+ # )
74
+
75
+ # project2 = harvest.projects.create_task(project, "A simple task")
76
+ # project2.should == project
77
+
78
+ # harvest.task_assignments.all(project).size.should == 1
79
+ # end
80
+ # end
81
+ # end
82
+ end
@@ -0,0 +1,85 @@
1
+ require 'spec_helper'
2
+
3
+ describe 'forecast users' do
4
+ # it "allows adding, updating, and removing users" do
5
+ # cassette("users") do
6
+ # user = harvest.users.create(
7
+ # "first_name" => "Edgar",
8
+ # "last_name" => "Ruth",
9
+ # "email" => "edgar@ruth.com",
10
+ # "timezone" => "cst",
11
+ # "is_admin" => "false",
12
+ # "telephone" => "444-4444"
13
+ # )
14
+ # user.id.should_not be_nil
15
+
16
+ # user.first_name = "Joey"
17
+ # user = harvest.users.update(user)
18
+ # user.first_name.should == "Joey"
19
+
20
+ # id = harvest.users.delete(user)
21
+ # harvest.users.all.map(&:id).should_not include(id)
22
+ # end
23
+ # end
24
+
25
+ # it "allows activating and deactivating users" do
26
+ # cassette("users2") do
27
+ # user = harvest.users.create(
28
+ # "first_name" => "John",
29
+ # "last_name" => "Ruth",
30
+ # "email" => "john@ruth.com",
31
+ # "timezone" => "cst",
32
+ # "is_admin" => "false",
33
+ # "telephone" => "444-4444"
34
+ # )
35
+ # user.should be_active
36
+
37
+ # user = harvest.users.deactivate(user)
38
+ # user.should_not be_active
39
+
40
+ # user = harvest.users.activate(user)
41
+ # user.should be_active
42
+
43
+ # harvest.users.delete(user)
44
+ # end
45
+ # end
46
+
47
+ # context "assignments" do
48
+ # it "allows adding, updating, and removing users from projects" do
49
+ # cassette('users4') do
50
+ # client = harvest.clients.create(
51
+ # "name" => "Joe's Steam Cleaning w/Users",
52
+ # "details" => "Building API Widgets across the country"
53
+ # )
54
+
55
+ # project = harvest.projects.create(
56
+ # "name" => "Test Project w/User",
57
+ # "active" => true,
58
+ # "notes" => "project to test the api",
59
+ # "client_id" => client.id
60
+ # )
61
+
62
+ # user = harvest.users.create(
63
+ # "first_name" => "Sally",
64
+ # "last_name" => "Ruth",
65
+ # "email" => "sally@ruth.com",
66
+ # "password" => "mypassword",
67
+ # "timezone" => "cst",
68
+ # "is_admin" => "false",
69
+ # "telephone" => "444-4444"
70
+ # )
71
+
72
+
73
+ # assignment = harvest.user_assignments.create("project" => project, "user" => user)
74
+
75
+ # assignment.hourly_rate = 100
76
+ # assignment = harvest.user_assignments.update(assignment)
77
+ # assignment.hourly_rate.should == 100.0
78
+
79
+ # harvest.user_assignments.delete(assignment)
80
+ # all_assignments = harvest.user_assignments.all(project)
81
+ # all_assignments.size.should == 1
82
+ # end
83
+ # end
84
+ # end
85
+ end
@@ -0,0 +1,41 @@
1
+ require 'spec_helper'
2
+
3
+ describe 'forecast projects' do
4
+
5
+ describe 'all' do
6
+ it 'works' do
7
+ cassette('projects-all') do
8
+ projects = forecast.projects.all
9
+
10
+ expect(projects.class).to be(Array)
11
+ expect(projects.size > 1).to eq(true)
12
+ end
13
+ end
14
+ end
15
+
16
+ describe 'find' do
17
+ it 'works' do
18
+ cassette('projects-find') do
19
+ projects = forecast.projects.all
20
+ project_id = projects.first.id
21
+ project = forecast.projects.find(project_id)
22
+
23
+ expect(project.class).to be(Forecast::Project)
24
+ expect(project.id > 1).to eq(true)
25
+ end
26
+ end
27
+ end
28
+
29
+ describe 'select_by_harvest_id()' do
30
+ it 'works' do
31
+ cassette('project-find_by_harvest_id') do
32
+ project = forecast.projects.find_by_harvest_id(mm_harvest_project_id)
33
+
34
+ expect(project.class).to be(Forecast::Project)
35
+ expect(project.id > 1).to eq(true)
36
+ expect(project.harvest_id).to eq(mm_harvest_project_id)
37
+ end
38
+ end
39
+ end
40
+
41
+ end
@@ -0,0 +1,41 @@
1
+ require 'forecasted'
2
+ require 'webmock/rspec'
3
+ require 'vcr'
4
+ require 'factory_girl'
5
+
6
+ require 'byebug' if ENV['TRAVIS'].nil?
7
+
8
+ Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each {|f| require File.expand_path(f) }
9
+
10
+ VCR.configure do |c|
11
+ c.cassette_library_dir = '.cassettes'
12
+ c.hook_into :webmock
13
+
14
+ c.default_cassette_options = {
15
+ # force cassettes to re_record when we pass VCR_REFRESH=true
16
+ re_record_interval: ENV['VCR_REFRESH'] == 'true' ? 0 : nil
17
+ }
18
+ end
19
+
20
+ FactoryGirl.find_definitions
21
+
22
+ RSpec.configure do |config|
23
+ config.include ForecastedHelpers
24
+
25
+ config.before(:suite) do
26
+ WebMock.allow_net_connect!
27
+ cassette("clean") do
28
+ ForecastedHelpers.clean_remote
29
+ end
30
+ end
31
+
32
+ config.before(:each) do
33
+ WebMock.allow_net_connect!
34
+ end
35
+
36
+ def cassette(*args)
37
+ VCR.use_cassette(*args) do
38
+ yield
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,6 @@
1
+ # DO NOT USE OUR REGULAR ACCOUNT'S CREDENTIALS HERE
2
+ # the test suite blasts all data on harvest before running
3
+ # the tests.
4
+ username: "my@email.com"
5
+ password: "secure"
6
+ subdomain: "my-domain"
@@ -0,0 +1,66 @@
1
+ module ForecastedHelpers
2
+
3
+ def mm_harvest_project_id
4
+ 11877152
5
+ end
6
+
7
+ def mm_forecast_project_id
8
+ 767184
9
+ end
10
+
11
+
12
+ def self.credentials
13
+ unless File.exist?("#{File.dirname(__FILE__)}/forecast_credentials.yml")
14
+ raise "You need a credentials file in support/forecast_credentials.yml!!"
15
+ end
16
+ @credentials ||= YAML.load_file("#{File.dirname(__FILE__)}/forecast_credentials.yml")
17
+ end
18
+
19
+ def self.simple_forecast
20
+ Forecast.client({
21
+ forecast_account_id: credentials["forecast_account_id"],
22
+ access_token: credentials["access_token"]
23
+ })
24
+ end
25
+
26
+ def credentials
27
+ ForecastedHelpers.credentials
28
+ end
29
+
30
+ def forecast
31
+ @forecast ||= ForecastedHelpers.simple_forecast
32
+ end
33
+
34
+ def hardy_forecast
35
+ Forecast.hardy_client(
36
+ forecast_account_id: credentials[:forecast_account_id],
37
+ access_token: credentials[:access_token]
38
+ )
39
+ end
40
+
41
+ def self.clean_remote
42
+ return unless ENV['TRAVIS'].nil?
43
+
44
+ forecast = simple_forecast
45
+
46
+ # harvest = simple_harvest
47
+ # forecast.users.all.each do |u|
48
+ # harvest.reports.expenses_by_user(u, Time.utc(2000, 1, 1), Time.utc(2014, 6, 21)).each do |expense|
49
+ # harvest.expenses.delete(expense, u)
50
+ # end
51
+
52
+ # harvest.reports.time_by_user(u, Time.utc(2000, 1, 1), Time.utc(2014, 6, 21)).each do |time|
53
+ # harvest.time.delete(time, u)
54
+ # end
55
+
56
+ # harvest.users.delete(u) if u.email != credentials["username"]
57
+ # end
58
+
59
+ # we store expenses on this date in the tests
60
+ # harvest.expenses.all(Time.utc(2009, 12, 28)).each {|e| harvest.expenses.delete(e) }
61
+
62
+ # %w(expense_categories time projects invoices contacts clients tasks).each do |collection|
63
+ # harvest.send(collection).all.each {|m| harvest.send(collection).delete(m) }
64
+ # end
65
+ end
66
+ end
@@ -0,0 +1,9 @@
1
+ shared_examples_for 'a json sanitizer' do |keys|
2
+ keys.each do |key|
3
+ it "doesn't include '#{key}' when serializing to json" do
4
+ instance = described_class.new
5
+ instance[key] = 10
6
+ instance.as_json[instance.class.json_root].keys.should_not include(key)
7
+ end
8
+ end
9
+ end