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