harv 0.8.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (38) hide show
  1. data/HISTORY +3 -0
  2. data/LICENSE +23 -0
  3. data/README.rdoc +173 -0
  4. data/Rakefile +44 -0
  5. data/lib/harvest/base.rb +118 -0
  6. data/lib/harvest/harvest_resource.rb +15 -0
  7. data/lib/harvest/plugins/active_resource_inheritable_headers.rb +36 -0
  8. data/lib/harvest/plugins/toggleable.rb +12 -0
  9. data/lib/harvest/resources/client.rb +34 -0
  10. data/lib/harvest/resources/entry.rb +34 -0
  11. data/lib/harvest/resources/expense.rb +22 -0
  12. data/lib/harvest/resources/expense_category.rb +7 -0
  13. data/lib/harvest/resources/person.rb +44 -0
  14. data/lib/harvest/resources/project.rb +51 -0
  15. data/lib/harvest/resources/task.rb +8 -0
  16. data/lib/harvest/resources/task_assignment.rb +27 -0
  17. data/lib/harvest/resources/user_assignment.rb +27 -0
  18. data/lib/harvest.rb +37 -0
  19. data/test/integration/client_integration.rb +17 -0
  20. data/test/integration/client_teardown.rb +11 -0
  21. data/test/integration/expense_category_integration.rb +16 -0
  22. data/test/integration/expense_category_teardown.rb +12 -0
  23. data/test/integration/harvest_integration_test.rb +47 -0
  24. data/test/integration/project_integration.rb +19 -0
  25. data/test/integration/project_teardown.rb +12 -0
  26. data/test/integration/task_integration.rb +19 -0
  27. data/test/integration/task_teardown.rb +12 -0
  28. data/test/test_helper.rb +51 -0
  29. data/test/unit/base_test.rb +84 -0
  30. data/test/unit/resources/client_test.rb +82 -0
  31. data/test/unit/resources/expense_category_test.rb +49 -0
  32. data/test/unit/resources/expense_test.rb +14 -0
  33. data/test/unit/resources/person_test.rb +150 -0
  34. data/test/unit/resources/project_test.rb +154 -0
  35. data/test/unit/resources/task_assignment_test.rb +72 -0
  36. data/test/unit/resources/task_test.rb +82 -0
  37. data/test/unit/resources/user_assignment_test.rb +71 -0
  38. metadata +137 -0
data/HISTORY ADDED
@@ -0,0 +1,3 @@
1
+ 0.8 (December 10, 2008)
2
+ First version
3
+
data/LICENSE ADDED
@@ -0,0 +1,23 @@
1
+ Copyright (c) 2008, Kyle Banker, Alexander Interactive, Inc.
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ of this software and associated documentation files (the "Software"), to deal
5
+ in the Software without restriction, including without limitation the rights
6
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ copies of the Software, and to permit persons to whom the Software is
8
+ furnished to do so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in
11
+ all copies or substantial portions of the Software.
12
+
13
+ Except as contained in this notice, the name(s) of the above copyright holders
14
+ shall not be used in advertising or otherwise to promote the sale, use or other
15
+ dealings in this Software without prior written authorization.
16
+
17
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
23
+ THE SOFTWARE.
data/README.rdoc ADDED
@@ -0,0 +1,173 @@
1
+ = harvest
2
+
3
+ A ruby library wrapping (most of) the Harvest Api.
4
+
5
+ == DESCRIPTION:
6
+
7
+ This library uses ActiveResource and activeresource_throttle to provide simple, reliable access to the Harvest Api. Good for building reporting scripts and simple apps.
8
+
9
+ Note that this library does not support the complete Harvest Api at this time. See Features/Issues below for details.
10
+
11
+ == USAGE:
12
+
13
+ === Creating a new harvest object:
14
+
15
+ All calls to the Harvest API will originate from a Harvest object. Initialize it like so:
16
+
17
+ @harvest = Harvest(:email => "joe@example.com",
18
+ :password => "secret",
19
+ :sub_domain => "joeandcompany",
20
+ :headers => {"User-Agent" => "MyCompany"})
21
+
22
+ The _headers_ argument is optional. Use <b>:ssl => true</b> if your account requires HTTPS.
23
+
24
+ === Clients
25
+
26
+ Index
27
+ @harvest.clients.find(:all)
28
+ Show
29
+ @harvest.clients.find(43235)
30
+ Create
31
+ @client = @harvest.clients.new
32
+ @client.attributes = {:name => "Company, LLC", :details => "New York, NY"}
33
+ @client.save
34
+ Update
35
+ @client.first_name = "Smith"
36
+ @client.save
37
+ Destroy
38
+ @client.destroy
39
+
40
+ Toggle (Active/Inactive)
41
+ @client.toggle
42
+
43
+ === Tasks
44
+
45
+ Index
46
+ @harvest.tasks.find(:all)
47
+ Show
48
+ @harvest.tasks.find(123)
49
+ Create
50
+ @task = @harvest.tasks.new
51
+ @task.attributes = {:name => "Meeting", :billable_by_default => false,
52
+ :is_default => false, :default_hourly_rate => 100}
53
+ @task.save
54
+ Update
55
+ @task.name = "Client Meeting"
56
+ @task.save
57
+ Destroy
58
+ @task.destroy
59
+
60
+ === People
61
+
62
+ Index
63
+ @harvest.people.find(:all)
64
+ Show
65
+ @harvest.people.find(123)
66
+
67
+ === Projects
68
+
69
+ Index
70
+ @harvest.projects.find(:all)
71
+ Show
72
+ @harvest.projects.find(123)
73
+ Create
74
+ @project = @harvest.projects.new
75
+ @project.attributes = {:name => "Refactor", :active => true,
76
+ :bill_by => "None", :client_id => @client.id, }
77
+ @project.save
78
+ Update
79
+ @project.name = "Mega-Refactor"
80
+ @project.save
81
+ Destroy
82
+ @project.destroy
83
+
84
+ Toggle (Active/Inactive)
85
+ @project.toggle
86
+
87
+ === Invoices
88
+
89
+ Index
90
+ @invoices = @harvest.invoices.find(:all)
91
+
92
+ Show
93
+ @invoice = @harvest.invoices.find(1)
94
+
95
+ Find By Invoice Number
96
+ @invoice = @harvest.invoices.find_by_number('200')
97
+
98
+ Create
99
+ @invoice = @harvest.invoices.new
100
+ @invoice.attributes = {:amount => '250.32', :client_id => @client.id}
101
+ @invoice.save
102
+
103
+ Update
104
+ @invoice.notes = "Pending payment"
105
+ @invoice.save
106
+
107
+ Destroy
108
+ @invoice.destroy
109
+
110
+ Return parsed csv line items
111
+ @invoice.parsed_csv_line_items
112
+
113
+ ==== User and Task Assignments
114
+
115
+ @project.users.find(:all)
116
+ @project.users.find(:first)
117
+
118
+ @project.tasks.find(:all)
119
+ @project.tasks.find(:first)
120
+
121
+ === Reports
122
+
123
+ @project.entries(:from => Time.now, :to => Time.now)
124
+ @project.entries(:from => Time.now, :to => Time.now, :user_id => @person.id)
125
+
126
+ @person.entries(:from => Time.now, :to => Time.now)
127
+ @person.expenses(:from => Time.now, :to => Time.now)
128
+
129
+
130
+ == FEATURES/PROBLEMS:
131
+
132
+ Supports most of the Harvest Api (http://www.getharvest.com/api).
133
+
134
+ Though RESTful, the Harvest Api does not entirely follow ActiveResource conventions. In order to support the complete API, certain customizations will be required. <b>We welcome any contributions/modifications to help complete this Api.</b>
135
+
136
+ The following Api features are incomplete due to incompatibilities with ActiveResource conventions (CRUD actions in parentheses):
137
+
138
+ 1. People (CUD)
139
+ 2. User & Task Assignments (CUD)
140
+ 3. Expenses (CUD)
141
+
142
+ In addition, the following have not been implemented at all due to time constraints:
143
+
144
+ 1. Time Tracking Api
145
+ 2. Invoices, Invoice Payments, Invoice Categories, and Invoice Messages
146
+
147
+ Expect a more complete library in the near future. Again, contributions are welcome.
148
+
149
+ == TEST SUITE
150
+
151
+ This library includes a fairly thorough set of unit and integration tests. The unit tests can be run with the "rake" command.
152
+
153
+ The integration tests require a Harvest account. It is best to use a trial account when running these tests.
154
+
155
+ The credentials may be entered in test/test_helper.rb. To run the integration suite:
156
+
157
+ rake test:integration
158
+
159
+ Note that the integration test structure is somewhat unorthodox, as it requires tests to run in a particular order. The main test file is test/integration/harvest_integration_test.rb. Examine this to see how the integration suite works.
160
+
161
+ == REQUIREMENTS:
162
+
163
+ Requires active_resource >= 2.1 and activeresource_throttle >= 1.0.
164
+
165
+ active_resource_throttle should install automaticallt, but just in case you need to install manually:
166
+
167
+ gem sources -a http://gems.github.com
168
+ sudo gem install aiaio-active_resource_throttle
169
+
170
+ == INSTALL:
171
+
172
+ gem sources -a http://gems.github.com
173
+ gem install aiaio-harvest
data/Rakefile ADDED
@@ -0,0 +1,44 @@
1
+ # -*- ruby -*-
2
+
3
+ require 'rubygems'
4
+ require 'rake'
5
+ require 'rake/testtask'
6
+ require './lib/harvest.rb'
7
+
8
+ desc 'Default: run all tests.'
9
+ task :default => :test_all
10
+
11
+ namespace :test do
12
+
13
+ Rake::TestTask.new(:resources) do |t|
14
+ t.libs << 'lib'
15
+ t.pattern = 'test/unit/resources/*_test.rb'
16
+ t.verbose = false
17
+ end
18
+
19
+ Rake::TestTask.new(:base) do |t|
20
+ t.libs << 'lib'
21
+ t.pattern = 'test/unit/*_test.rb'
22
+ t.verbose = false
23
+ end
24
+
25
+ Rake::TestTask.new(:integration) do |t|
26
+ t.libs << 'lib'
27
+ t.pattern = 'test/integration/*_test.rb'
28
+ t.verbose = false
29
+ end
30
+
31
+ end
32
+
33
+ desc "Test everything."
34
+ task :test_all do
35
+ errors = %w(test:resources test:base).collect do |task|
36
+ begin
37
+ Rake::Task[task].invoke
38
+ nil
39
+ rescue => e
40
+ task
41
+ end
42
+ end.compact
43
+ abort "Errors running #{errors.to_sentence}!" if errors.any?
44
+ end
@@ -0,0 +1,118 @@
1
+ module Harvest
2
+ class Base
3
+ @debug_level = 0
4
+
5
+ def self.debug_level
6
+ @debug_level
7
+ end
8
+
9
+ def self.debug_level=(debug_level)
10
+ raise ArgumentError, "debug level must be an integer" unless debug_level == debug_level.to_i
11
+
12
+ return @debug_level if @debug_level == debug_level
13
+
14
+ @debug_level = debug_level.to_i
15
+
16
+ ActiveSupport::Notifications.unsubscribe(@subscriber) if @subscriber
17
+
18
+ case @debug_level
19
+ when 0 then
20
+ when 1 then
21
+ @subscriber = ActiveSupport::Notifications.subscribe(/request.active_resource/) do |*args|
22
+ event = ActiveSupport::Notifications::Event.new(*args)
23
+ puts "-- HARVEST #{event.payload[:method].to_s.upcase} #{event.payload[:request_uri]}, #{event.payload[:result].andand.code}: #{event.payload[:result].andand.message}"
24
+ end
25
+ else
26
+ @subscriber = ActiveSupport::Notifications.subscribe(/request.active_resource/) do |*args|
27
+ event = ActiveSupport::Notifications::Event.new(*args)
28
+ puts "-- HARVEST #{event.payload[:method].to_s.upcase} #{event.payload[:request_uri]}, #{event.payload[:result].andand.code}: #{event.payload[:result].andand.message}"
29
+ puts event.payload[:result].body + "\n"
30
+ end
31
+ end
32
+
33
+ @debug_level
34
+ end
35
+
36
+ # Requires a sub_domain, email, and password.
37
+ # Specifying headers is optional, but useful for setting a user agent.
38
+ def initialize(options={})
39
+ options.assert_required_keys(:email, :password, :sub_domain)
40
+ @email = options[:email]
41
+ @password = options[:password]
42
+ @sub_domain = options[:sub_domain]
43
+ @headers = options[:headers]
44
+ @ssl = options[:ssl]
45
+ configure_base_resource
46
+ end
47
+
48
+ # Below is a series of proxies allowing for easy
49
+ # access to the various resources.
50
+
51
+ # Clients
52
+ def clients
53
+ Harvest::Resources::Client
54
+ end
55
+
56
+ # Contacts
57
+ def contacts
58
+ Harvest::Resources::Contact
59
+ end
60
+
61
+ # Expenses.
62
+ def expenses
63
+ Harvest::Resources::Expense
64
+ end
65
+
66
+ # Expense categories.
67
+ def expense_categories
68
+ Harvest::Resources::ExpenseCategory
69
+ end
70
+
71
+ # People.
72
+ # Also provides access to time entries.
73
+ def people
74
+ Harvest::Resources::Person
75
+ end
76
+
77
+ # Projects.
78
+ # Provides access to the assigned users and tasks
79
+ # along with reports for entries on the project.
80
+ def projects
81
+ Harvest::Resources::Project
82
+ end
83
+
84
+ # Tasks.
85
+ def tasks
86
+ Harvest::Resources::Task
87
+ end
88
+
89
+ # Invoices
90
+ def invoices
91
+ Harvest::Resources::Invoice
92
+ end
93
+
94
+ # Invoice Messages
95
+ def invoice_messages
96
+ Harvest::Resources::InvoiceMessage
97
+ end
98
+
99
+ private
100
+
101
+ # Configure resource base class so that
102
+ # inherited classes can access the api.
103
+ def configure_base_resource
104
+ HarvestResource.site = "http#{'s' if @ssl}://#{@sub_domain}.#{Harvest::ApiDomain}"
105
+ HarvestResource.user = @email
106
+ HarvestResource.password = @password
107
+ HarvestResource.headers.update(@headers) if @headers.is_a?(Hash)
108
+ load_resources
109
+ end
110
+
111
+ # Load the classes representing the various resources.
112
+ def load_resources
113
+ resource_path = File.join(File.dirname(__FILE__), "resources")
114
+ Harvest.load_all_ruby_files_from_path(resource_path)
115
+ end
116
+
117
+ end
118
+ end
@@ -0,0 +1,15 @@
1
+ module Harvest
2
+ # This is the base class from which all resource
3
+ # classes inherit. Site and authentication params
4
+ # are loaded into this class when a Harvest::Base
5
+ # object is initialized.
6
+ class HarvestResource < ActiveResource::Base
7
+ include ActiveResourceThrottle
8
+ include Harvest::Plugins::ActiveResourceInheritableHeaders
9
+
10
+ # The harvest api will block requests in excess
11
+ # of 40 / 15 seconds. Adds a throttle (with cautious settings).
12
+ # Throttle feature provided by activeresource_throttle gem.
13
+ self.throttle(:requests => 30, :interval => 15, :sleep_interval => 60)
14
+ end
15
+ end
@@ -0,0 +1,36 @@
1
+ # Allows headers in an ActiveResource::Base class
2
+ # to be inherited by subclasses. Useful for setting
3
+ # a User-Agent used by all resources.
4
+ module Harvest
5
+ module Plugins
6
+ module ActiveResourceInheritableHeaders
7
+
8
+ module ClassMethods
9
+
10
+ # If headers are not defined in a
11
+ # given subclass, then obtain headers
12
+ # from the superclass.
13
+ def inheritable_headers
14
+ if defined?(@headers)
15
+ @headers
16
+ elsif superclass != Object && superclass.headers
17
+ superclass.headers
18
+ else
19
+ @headers ||= {}
20
+ end
21
+ end
22
+
23
+ end
24
+
25
+ def self.included(klass)
26
+ klass.instance_eval do
27
+ klass.extend ClassMethods
28
+ class << self
29
+ alias_method :headers, :inheritable_headers
30
+ end
31
+ end
32
+ end
33
+
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,12 @@
1
+ # Adds toggability to a harvest resource.
2
+ module Harvest
3
+ module Plugins
4
+ module Toggleable
5
+
6
+ def toggle
7
+ put(:toggle)
8
+ end
9
+
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,34 @@
1
+ module Harvest
2
+ module Resources
3
+ class Client < Harvest::HarvestResource
4
+ include Harvest::Plugins::Toggleable
5
+
6
+ def invoices(refresh = false)
7
+ if not refresh and @invoices
8
+ @invoices
9
+ else
10
+ page, @invoices = 1, []
11
+ begin
12
+ set = Invoice.find(:all, :params => {:client => self.id, :page => page })
13
+ puts "Found #{set.length.to_s} invoices" if Harvest::Base.debug_level == 2
14
+ @invoices += set
15
+ page +=1
16
+ end while set.length == 50
17
+ @invoices
18
+ end
19
+ end
20
+
21
+ def balance
22
+ invoices.select {|invoice| invoice.sent? }.inject(0) {|total, invoice| total + invoice.balance}
23
+ end
24
+
25
+ def invoiced_amount
26
+ invoices.select {|invoice| invoice.sent? }.inject(0) {|total, invoice| total + invoice.amount}
27
+ end
28
+
29
+ def paid_amount
30
+ invoices.select {|invoice| invoice.sent? }.inject(0) {|total, invoice| total + invoice.paid_amount}
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,34 @@
1
+ # The entry resource is never accessed directly;
2
+ # rather, it is manipulated through an instance
3
+ # of Project or Person.
4
+ module Harvest
5
+ module Resources
6
+ class Entry < Harvest::HarvestResource
7
+
8
+ self.element_name = "entry"
9
+
10
+ class << self
11
+
12
+ def project_id=(id)
13
+ @project_id = id
14
+ self.site = self.site + "/projects/#{@project_id}"
15
+ end
16
+
17
+ def project_id
18
+ @project_id
19
+ end
20
+
21
+ def person_id=(id)
22
+ @person_id = id
23
+ self.site = self.site + "/people/#{@person_id}"
24
+ end
25
+
26
+ def person_id
27
+ @person_id
28
+ end
29
+
30
+ end
31
+
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,22 @@
1
+ module Harvest
2
+ module Resources
3
+ class Expense < Harvest::HarvestResource
4
+
5
+ self.element_name = "expense"
6
+
7
+ class << self
8
+
9
+ def person_id=(id)
10
+ @person_id = id
11
+ self.site = self.site + "/people/#{@person_id}"
12
+ end
13
+
14
+ def person_id
15
+ @person_id
16
+ end
17
+
18
+ end
19
+
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,7 @@
1
+ module Harvest
2
+ module Resources
3
+ class ExpenseCategory < Harvest::HarvestResource
4
+
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,44 @@
1
+ module Harvest
2
+ module Resources
3
+ class Person < Harvest::HarvestResource
4
+ include Harvest::Plugins::Toggleable
5
+
6
+ # Find all entries for the given person;
7
+ # options[:from] and options[:to] are required;
8
+ # include options[:user_id] to limit by a specific project.
9
+ def entries(options={})
10
+ validate_options(options)
11
+ entry_class = Harvest::Resources::Entry.clone
12
+ entry_class.person_id = self.id
13
+ entry_class.find :all, :params => format_params(options)
14
+ end
15
+
16
+ def expenses(options={})
17
+ validate_options(options)
18
+ expense_class = Harvest::Resources::Expense.clone
19
+ expense_class.person_id = self.id
20
+ expense_class.find :all, :params => format_params(options)
21
+ end
22
+
23
+ private
24
+
25
+ def validate_options(options)
26
+ if [:from, :to].any? {|key| !options[key].respond_to?(:strftime) }
27
+ raise ArgumentError, "Must specify :from and :to as dates."
28
+ end
29
+
30
+ if options[:from] > options[:to]
31
+ raise ArgumentError, ":start must precede :end."
32
+ end
33
+
34
+ end
35
+ def format_params(options)
36
+ ops = { :from => options[:from].strftime("%Y%m%d"),
37
+ :to => options[:to].strftime("%Y%m%d")}
38
+ ops[:project_id] = options[:project_id] if options[:project_id]
39
+ return ops
40
+ end
41
+
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,51 @@
1
+ module Harvest
2
+ module Resources
3
+ # Supports the following:
4
+ class Project < Harvest::HarvestResource
5
+ include Harvest::Plugins::Toggleable
6
+
7
+ def users
8
+ user_class = Harvest::Resources::UserAssignment.clone
9
+ user_class.project_id = self.id
10
+ user_class
11
+ end
12
+
13
+ def tasks
14
+ task_class = Harvest::Resources::TaskAssignment.clone
15
+ task_class.project_id = self.id
16
+ task_class
17
+ end
18
+
19
+ # Find all entries for the given project;
20
+ # options[:from] and options[:to] are required;
21
+ # include options[:user_id] to limit by a specific user.
22
+ #
23
+ def entries(options={})
24
+ validate_entries_options(options)
25
+ entry_class = Harvest::Resources::Entry.clone
26
+ entry_class.project_id = self.id
27
+ entry_class.find :all, :params => format_params(options)
28
+ end
29
+
30
+ private
31
+
32
+ def validate_entries_options(options)
33
+ if [:from, :to].any? {|key| !options[key].respond_to?(:strftime) }
34
+ raise ArgumentError, "Must specify :from and :to as dates."
35
+ end
36
+
37
+ if options[:from] > options[:to]
38
+ raise ArgumentError, ":start must precede :end."
39
+ end
40
+ end
41
+
42
+ def format_params(options)
43
+ ops = { :from => options[:from].strftime("%Y%m%d"),
44
+ :to => options[:to].strftime("%Y%m%d")}
45
+ ops[:user_id] = options[:user_id] if options[:user_id]
46
+ return ops
47
+ end
48
+
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,8 @@
1
+ module Harvest
2
+ module Resources
3
+ class Task < Harvest::HarvestResource
4
+ include Harvest::Plugins::Toggleable
5
+
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,27 @@
1
+ # This class is accessed by an instance of Project.
2
+ module Harvest
3
+ module Resources
4
+ class TaskAssignment < Harvest::HarvestResource
5
+
6
+ self.element_name = "task_assignment"
7
+
8
+ class << self
9
+
10
+ def project_id=(id)
11
+ @project_id = id
12
+ set_site
13
+ end
14
+
15
+ def project_id
16
+ @project_id
17
+ end
18
+
19
+ def set_site
20
+ self.site = self.site + "/projects/#{self.project_id}"
21
+ end
22
+
23
+ end
24
+
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,27 @@
1
+ # This class is accessed by an instance of Project.
2
+ module Harvest
3
+ module Resources
4
+ class UserAssignment < Harvest::HarvestResource
5
+
6
+ self.element_name = "user_assignment"
7
+
8
+ class << self
9
+
10
+ def project_id=(id)
11
+ @project_id = id
12
+ set_site
13
+ end
14
+
15
+ def project_id
16
+ @project_id
17
+ end
18
+
19
+ def set_site
20
+ self.site = self.site + "/projects/#{self.project_id}"
21
+ end
22
+
23
+ end
24
+
25
+ end
26
+ end
27
+ end