cashboard 1.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 (62) hide show
  1. data/.autotest +26 -0
  2. data/.gitignore +2 -0
  3. data/LICENSE +19 -0
  4. data/README.textile +58 -0
  5. data/Rakefile +25 -0
  6. data/cashboard.gemspec +129 -0
  7. data/examples/create_account.rb +38 -0
  8. data/examples/list_stuff_in_account.rb +15 -0
  9. data/examples/simple_workflow.rb +35 -0
  10. data/examples/time_tracking.rb +30 -0
  11. data/examples/toggle_timer.rb +0 -0
  12. data/lib/cashboard/account.rb +21 -0
  13. data/lib/cashboard/base.rb +223 -0
  14. data/lib/cashboard/behaviors/base.rb +4 -0
  15. data/lib/cashboard/behaviors/lists_line_items.rb +32 -0
  16. data/lib/cashboard/behaviors/toggleable.rb +18 -0
  17. data/lib/cashboard/client_company.rb +25 -0
  18. data/lib/cashboard/client_contact.rb +32 -0
  19. data/lib/cashboard/company_membership.rb +6 -0
  20. data/lib/cashboard/document_template.rb +10 -0
  21. data/lib/cashboard/employee.rb +29 -0
  22. data/lib/cashboard/errors.rb +48 -0
  23. data/lib/cashboard/estimate.rb +43 -0
  24. data/lib/cashboard/expense.rb +14 -0
  25. data/lib/cashboard/invoice.rb +75 -0
  26. data/lib/cashboard/invoice_line_item.rb +15 -0
  27. data/lib/cashboard/invoice_payment.rb +7 -0
  28. data/lib/cashboard/line_item.rb +27 -0
  29. data/lib/cashboard/payment.rb +22 -0
  30. data/lib/cashboard/project.rb +38 -0
  31. data/lib/cashboard/project_assignment.rb +9 -0
  32. data/lib/cashboard/time_entry.rb +45 -0
  33. data/lib/cashboard/version.rb +3 -0
  34. data/lib/cashboard.rb +75 -0
  35. data/lib/typecasted_open_struct.rb +82 -0
  36. data/test/fixtures/account.xml +50 -0
  37. data/test/fixtures/cashboard_credentials.yml +3 -0
  38. data/test/fixtures/client_companies.xml +41 -0
  39. data/test/fixtures/client_contacts.xml +53 -0
  40. data/test/fixtures/company_memberships.xml +21 -0
  41. data/test/fixtures/document_templates.xml +53 -0
  42. data/test/fixtures/employees.xml +51 -0
  43. data/test/fixtures/estimates.xml +243 -0
  44. data/test/fixtures/expenses.xml +101 -0
  45. data/test/fixtures/invoice_line_items.xml +138 -0
  46. data/test/fixtures/invoice_payments.xml +10 -0
  47. data/test/fixtures/invoices.xml +231 -0
  48. data/test/fixtures/line_items.xml +243 -0
  49. data/test/fixtures/payments.xml +93 -0
  50. data/test/fixtures/project_assignments.xml +30 -0
  51. data/test/fixtures/projects.xml +129 -0
  52. data/test/fixtures/time_entries.xml +213 -0
  53. data/test/full.rb +3 -0
  54. data/test/test_helper.rb +112 -0
  55. data/test/unit/account_test.rb +85 -0
  56. data/test/unit/document_template_test.rb +18 -0
  57. data/test/unit/estimate_test.rb +166 -0
  58. data/test/unit/expense_test.rb +16 -0
  59. data/test/unit/invoice_test.rb +185 -0
  60. data/test/unit/project_test.rb +198 -0
  61. data/test/unit/time_entry_test.rb +225 -0
  62. metadata +220 -0
data/.autotest ADDED
@@ -0,0 +1,26 @@
1
+ require 'redgreen/autotest'
2
+
3
+ Autotest.add_hook :initialize do |at|
4
+ # Ignore these files
5
+ %w{
6
+ .hg .git .svn stories tmtags Rakefile Capfile README .html
7
+ spec/spec.opts spec/rcov.opts vendor/gems autotest svn-commit .DS_Store
8
+ }.each {|exception|at.add_exception(exception)}
9
+
10
+ at.clear_mappings
11
+
12
+ # Test everything in cashboard/lib
13
+ at.add_mapping(%r%^lib/cashboard/(.*)\.rb$%) do |file, m|
14
+ # also run original class file
15
+ full_dir = File.dirname(file)
16
+ orig = full_dir[full_dir.rindex('/')+1, full_dir.length]
17
+ model = File.basename(file, '.rb')
18
+
19
+ at.files_matching %r%^test/unit/#{model}_test\.rb$%
20
+ end
21
+
22
+ # Now add support for the test files themselves
23
+ at.add_mapping(%r%^test/unit/(.*)\.rb$%) do |file, m|
24
+ at.files_matching %r%#{file}%
25
+ end
26
+ end
data/.gitignore ADDED
@@ -0,0 +1,2 @@
1
+ # Ignore built gems from jeweler
2
+ pkg
data/LICENSE ADDED
@@ -0,0 +1,19 @@
1
+ Copyright (c) 2010 Subimage LLC
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
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
+ THE SOFTWARE.
data/README.textile ADDED
@@ -0,0 +1,58 @@
1
+ h1. Cashboard-rb
2
+
3
+ This project is the official Ruby wrapper for the "Cashboard API":http://api.cashboardapp.com created and maintained by "Subimage LLC":http://www.subimage.com.
4
+
5
+ "Cashboard":http://www.getcashboard.com is an online service that allows you to track time, create estimates, invoice your clients, and collect payments via credit cards or PayPal.
6
+
7
+
8
+ h2. Installation
9
+
10
+ bc. sudo gem install cashboard
11
+
12
+
13
+ h2. Example Code
14
+
15
+ bc.. require 'rubygems'
16
+ require 'cashboard'
17
+
18
+ Cashboard::Base.authenticate('your_subdomain', 'your_api_key')
19
+
20
+ puts "Employees:"
21
+ Cashboard::Employee.list.each { |emp| puts emp.inspect }
22
+
23
+ puts "Client Companies:"
24
+ Cashboard::ClientCompany.list.each { |client| puts client.inspect }
25
+
26
+ puts "Projects:"
27
+ Cashboard::Project.list.each { |prj| puts prj.inspect }
28
+
29
+ p. There are more examples in the /examples directory of this project.
30
+
31
+
32
+ h2. Links
33
+
34
+ * "Cashboard API Documentation":http://api.cashboardapp.com
35
+ * "Cashboard API Forum":http://forum.getcashboard.com/forums/7
36
+ * "Source code for Cashboard-rb":http://github.com/subimage/cashboard-rb
37
+
38
+
39
+ h2. Contributing
40
+
41
+ If you'd like to contribute to this project, please fork it and add your code *with working tests*.
42
+
43
+
44
+ h3. Testing
45
+
46
+ Tests are handled with Test::Unit and Fakeweb.
47
+ There's a complete test suite you can run by invoking:
48
+ @ruby test/full.rb@
49
+
50
+ The project is also setup to use autotest.
51
+
52
+ If you add any code please ensure all tests continue to run properly by running the test suite.
53
+
54
+
55
+ h2. License
56
+
57
+ Cashboard-rb is released under the MIT License.
58
+ Tweak it, fork it, twerk it and translate it to other languages...please!
data/Rakefile ADDED
@@ -0,0 +1,25 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+
4
+ require File.dirname(__FILE__) + "/lib/cashboard"
5
+
6
+ begin
7
+ require 'jeweler'
8
+ Jeweler::Tasks.new do |gem|
9
+ gem.name = "cashboard"
10
+ gem.summary = "Ruby wrapper library for the Cashboard API "
11
+ gem.description = "For more information see the homepage. Support and discussion can be found on the Cashboard forum (http://forum.getcashboard.com/)"
12
+ gem.email = "support@getcashboard.com"
13
+ gem.homepage = "http://github.com/subimage/cashboard-rb"
14
+ gem.authors = ["Subimage LLC"]
15
+ gem.version = Cashboard::VERSION
16
+ gem.add_development_dependency('mocha', '>= 0.9.8')
17
+ gem.add_development_dependency('fakeweb', '>= 1.2.8')
18
+ gem.add_dependency('activesupport', '>= 2.3.5')
19
+ gem.add_dependency('httparty', '>= 0.6.1')
20
+ gem.add_dependency('xml-simple', '>= 1.0.12')
21
+ end
22
+ Jeweler::GemcutterTasks.new
23
+ rescue LoadError
24
+ puts "Jeweler not available. Install it with: gem install jeweler"
25
+ end
data/cashboard.gemspec ADDED
@@ -0,0 +1,129 @@
1
+ # Generated by jeweler
2
+ # DO NOT EDIT THIS FILE DIRECTLY
3
+ # Instead, edit Jeweler::Tasks in Rakefile, and run the gemspec command
4
+ # -*- encoding: utf-8 -*-
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = %q{cashboard}
8
+ s.version = "1.0.1"
9
+
10
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
+ s.authors = ["Subimage LLC"]
12
+ s.date = %q{2010-07-29}
13
+ s.description = %q{For more information see the homepage. Support and discussion can be found on the Cashboard forum (http://forum.getcashboard.com/)}
14
+ s.email = %q{support@getcashboard.com}
15
+ s.extra_rdoc_files = [
16
+ "LICENSE",
17
+ "README.textile"
18
+ ]
19
+ s.files = [
20
+ ".autotest",
21
+ ".gitignore",
22
+ "LICENSE",
23
+ "README.textile",
24
+ "Rakefile",
25
+ "cashboard.gemspec",
26
+ "examples/create_account.rb",
27
+ "examples/list_stuff_in_account.rb",
28
+ "examples/simple_workflow.rb",
29
+ "examples/time_tracking.rb",
30
+ "examples/toggle_timer.rb",
31
+ "lib/cashboard.rb",
32
+ "lib/cashboard/account.rb",
33
+ "lib/cashboard/base.rb",
34
+ "lib/cashboard/behaviors/base.rb",
35
+ "lib/cashboard/behaviors/lists_line_items.rb",
36
+ "lib/cashboard/behaviors/toggleable.rb",
37
+ "lib/cashboard/client_company.rb",
38
+ "lib/cashboard/client_contact.rb",
39
+ "lib/cashboard/company_membership.rb",
40
+ "lib/cashboard/document_template.rb",
41
+ "lib/cashboard/employee.rb",
42
+ "lib/cashboard/errors.rb",
43
+ "lib/cashboard/estimate.rb",
44
+ "lib/cashboard/expense.rb",
45
+ "lib/cashboard/invoice.rb",
46
+ "lib/cashboard/invoice_line_item.rb",
47
+ "lib/cashboard/invoice_payment.rb",
48
+ "lib/cashboard/line_item.rb",
49
+ "lib/cashboard/payment.rb",
50
+ "lib/cashboard/project.rb",
51
+ "lib/cashboard/project_assignment.rb",
52
+ "lib/cashboard/time_entry.rb",
53
+ "lib/cashboard/version.rb",
54
+ "lib/typecasted_open_struct.rb",
55
+ "test/fixtures/account.xml",
56
+ "test/fixtures/cashboard_credentials.yml",
57
+ "test/fixtures/client_companies.xml",
58
+ "test/fixtures/client_contacts.xml",
59
+ "test/fixtures/company_memberships.xml",
60
+ "test/fixtures/document_templates.xml",
61
+ "test/fixtures/employees.xml",
62
+ "test/fixtures/estimates.xml",
63
+ "test/fixtures/expenses.xml",
64
+ "test/fixtures/invoice_line_items.xml",
65
+ "test/fixtures/invoice_payments.xml",
66
+ "test/fixtures/invoices.xml",
67
+ "test/fixtures/line_items.xml",
68
+ "test/fixtures/payments.xml",
69
+ "test/fixtures/project_assignments.xml",
70
+ "test/fixtures/projects.xml",
71
+ "test/fixtures/time_entries.xml",
72
+ "test/full.rb",
73
+ "test/test_helper.rb",
74
+ "test/unit/account_test.rb",
75
+ "test/unit/document_template_test.rb",
76
+ "test/unit/estimate_test.rb",
77
+ "test/unit/expense_test.rb",
78
+ "test/unit/invoice_test.rb",
79
+ "test/unit/project_test.rb",
80
+ "test/unit/time_entry_test.rb"
81
+ ]
82
+ s.homepage = %q{http://github.com/subimage/cashboard-rb}
83
+ s.rdoc_options = ["--charset=UTF-8"]
84
+ s.require_paths = ["lib"]
85
+ s.rubygems_version = %q{1.3.7}
86
+ s.summary = %q{Ruby wrapper library for the Cashboard API}
87
+ s.test_files = [
88
+ "test/full.rb",
89
+ "test/test_helper.rb",
90
+ "test/unit/account_test.rb",
91
+ "test/unit/document_template_test.rb",
92
+ "test/unit/estimate_test.rb",
93
+ "test/unit/expense_test.rb",
94
+ "test/unit/invoice_test.rb",
95
+ "test/unit/project_test.rb",
96
+ "test/unit/time_entry_test.rb",
97
+ "examples/create_account.rb",
98
+ "examples/list_stuff_in_account.rb",
99
+ "examples/simple_workflow.rb",
100
+ "examples/time_tracking.rb",
101
+ "examples/toggle_timer.rb"
102
+ ]
103
+
104
+ if s.respond_to? :specification_version then
105
+ current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
106
+ s.specification_version = 3
107
+
108
+ if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
109
+ s.add_development_dependency(%q<mocha>, [">= 0.9.8"])
110
+ s.add_development_dependency(%q<fakeweb>, [">= 1.2.8"])
111
+ s.add_runtime_dependency(%q<activesupport>, [">= 2.3.5"])
112
+ s.add_runtime_dependency(%q<httparty>, [">= 0.6.1"])
113
+ s.add_runtime_dependency(%q<xml-simple>, [">= 1.0.12"])
114
+ else
115
+ s.add_dependency(%q<mocha>, [">= 0.9.8"])
116
+ s.add_dependency(%q<fakeweb>, [">= 1.2.8"])
117
+ s.add_dependency(%q<activesupport>, [">= 2.3.5"])
118
+ s.add_dependency(%q<httparty>, [">= 0.6.1"])
119
+ s.add_dependency(%q<xml-simple>, [">= 1.0.12"])
120
+ end
121
+ else
122
+ s.add_dependency(%q<mocha>, [">= 0.9.8"])
123
+ s.add_dependency(%q<fakeweb>, [">= 1.2.8"])
124
+ s.add_dependency(%q<activesupport>, [">= 2.3.5"])
125
+ s.add_dependency(%q<httparty>, [">= 0.6.1"])
126
+ s.add_dependency(%q<xml-simple>, [">= 1.0.12"])
127
+ end
128
+ end
129
+
@@ -0,0 +1,38 @@
1
+ # Shows a simple method to create an account via API
2
+ # then connect and list resources for that account.
3
+
4
+ require "../lib/cashboard"
5
+
6
+ # Create a new account from the API
7
+ # Creating an account doesn't require an authorized connection.
8
+ begin
9
+ acct = Cashboard::Account.create({
10
+ :subdomain => 'WuTang',
11
+ :currency_type => 'USD',
12
+ :date_format => 'mm_dd_yyyy',
13
+ :owner => {
14
+ :email_address => 'rza@wutang.com',
15
+ :first_name => 'The',
16
+ :last_name => 'Rza',
17
+ :password => '36chambers'
18
+ },
19
+ :company => {
20
+ :name => 'Wu Tang',
21
+ :address => '1 Shogun Way',
22
+ :city => 'Shaolin',
23
+ :state => 'NY'
24
+ }
25
+ })
26
+ rescue Cashboard::BadRequest => e
27
+ puts "Account creation failure"
28
+ puts e.errors.inspect
29
+ end
30
+
31
+ puts acct.inspect
32
+
33
+ # Connect to account after we've created it
34
+ Cashboard::Base.authenticate(acct.subdomain, acct.owner[:api_key])
35
+
36
+ # List all projects in the account (should be 1 by default)
37
+ puts "Projects:"
38
+ Cashboard::Project.list.each {|prj| puts prj.inspect }
@@ -0,0 +1,15 @@
1
+ # List various resources inside a Cashboard account
2
+ # and display them on the screen.
3
+
4
+ require "../lib/cashboard"
5
+
6
+ Cashboard::Base.authenticate('your_subdomain', 'your_api_key')
7
+
8
+ puts "Employees:"
9
+ Cashboard::Employee.list.each { |emp| puts emp.inspect }
10
+
11
+ puts "Client Companies:"
12
+ Cashboard::ClientCompany.list.each { |client| puts client.inspect }
13
+
14
+ puts "Projects:"
15
+ Cashboard::Project.list.each { |prj| puts prj.inspect }
@@ -0,0 +1,35 @@
1
+ # Simple workflow that:
2
+ # * Creates a client company
3
+ # * Creates a project for that client
4
+ # * Creates a task for the project
5
+ # * Logs some time
6
+
7
+ require "../lib/cashboard"
8
+
9
+ Cashboard::Base.authenticate('your_subdomain', 'your_api_key')
10
+
11
+ # Create a new client
12
+ client = Cashboard::ClientCompany.create(
13
+ :name => 'Bigtime Ventures'
14
+ )
15
+
16
+ # Create a new project for that client
17
+ project = Cashboard::Project.create(
18
+ :name => 'Bigtime Web Redesign',
19
+ :client_id => client[:id],
20
+ :client_type => 'Company'
21
+ )
22
+
23
+ # Create task for project
24
+ task = Cashboard::LineItem.create(
25
+ :project_id => project[:id],
26
+ :title => 'Graphic design'
27
+ )
28
+
29
+ # Track some time
30
+ time_entry = Cashboard::TimeEntry.create(
31
+ :created_on => "04/11/2010",
32
+ :minutes => 8*60,
33
+ :description => 'Trolled 4chan',
34
+ :task_id => task[:id]
35
+ )
@@ -0,0 +1,30 @@
1
+ require "../lib/cashboard"
2
+
3
+ Cashboard::Base.authenticate('your_subdomain', 'your_api_key')
4
+
5
+ # Grab reference to first task in account
6
+ prj = Cashboard::Project.list[0]
7
+
8
+ # Grab reference to first task
9
+ task = prj.tasks[0]
10
+
11
+ puts "Creating a time entry for..."
12
+ puts " Project: #{prj.name}"
13
+ puts " Task : #{task.title}\n\n"
14
+
15
+ # Create a time entry for it
16
+ te = Cashboard::TimeEntry.create({
17
+ :line_item_id => task.id,
18
+ :minutes => 60,
19
+ :description => "Doin work!"
20
+ })
21
+
22
+ # Show our handy work
23
+ puts te.inspect
24
+
25
+ # Remove it
26
+ if te.delete == true
27
+ puts "\nTime entry has been destroyed"
28
+ else
29
+ puts "\nTime entry could not be destroyed"
30
+ end
File without changes
@@ -0,0 +1,21 @@
1
+ module Cashboard
2
+ class Account < Base
3
+ # Account is the only singular resource for the Cashboard API.
4
+ def self.resource_name; 'account'; end
5
+
6
+ # We get some sub-resources that we need to turn into
7
+ # Cashboard::Structs so we can access their information
8
+ # in a friendly way.
9
+ def initialize(hash={})
10
+ super hash
11
+ self.owner = Cashboard::Struct.new(self.owner)
12
+ self.company = Cashboard::Struct.new(self.company)
13
+ self
14
+ end
15
+
16
+ def href
17
+ "/#{self.class.resource_name}"
18
+ end
19
+
20
+ end
21
+ end
@@ -0,0 +1,223 @@
1
+ module Cashboard
2
+ class Base < Cashboard::Struct
3
+ include HTTParty
4
+
5
+ if defined? TEST_ENVIRONMENT
6
+ @@api_url = "http://apicashboard.i"
7
+ else
8
+ @@api_url = "https://api.cashboardapp.com"
9
+ end
10
+
11
+ base_uri @@api_url
12
+
13
+ cattr_accessor :auth
14
+ cattr_accessor :api_url
15
+
16
+ # Stores id and url for resource when instantiated
17
+ attr_accessor :id
18
+ attr_accessor :href
19
+
20
+ # Sets authentication credentials for all following requests.
21
+ def self.authenticate(subdomain, api_key)
22
+ @@auth = {:username => subdomain, :password => api_key}
23
+ end
24
+
25
+ # Clears authentication credentials.
26
+ def self.clear_authentication
27
+ @@auth = {}
28
+ end
29
+
30
+ # Initializes an object of this type by passing a known URL in.
31
+ def self.new_from_url(url, options={})
32
+ response = get(url, merge_options(options))
33
+ check_status_code(response)
34
+ return self.new(response[resource_name])
35
+ end
36
+
37
+ # Lists all items for a resource.
38
+ #
39
+ # Returns array of objects of the type listed.
40
+ # raises error if something goes wrong.
41
+ def self.list(options={})
42
+ self.get_collection("/#{resource_name}", self, options)
43
+ end
44
+
45
+ # Creates a resource.
46
+ #
47
+ # Returns object of type created if success, or raise error
48
+ # if something went wrong.
49
+ #
50
+ # Allows you to pass in a hash to create without naming it.
51
+ #
52
+ # Example:
53
+ #
54
+ # te = Cashboard::TimeEntry.create({
55
+ # :minutes => 60, :project_id => 12345
56
+ # })
57
+ def self.create(params={}, options={})
58
+ options = merge_options(options)
59
+ options.merge!({:body => self.new(params).to_xml})
60
+ response = post("/#{resource_name}", options)
61
+ check_status_code(response)
62
+ return self.new(response.parsed_response)
63
+ end
64
+
65
+ # INSTANCE METHODS ========================================================
66
+
67
+ # Override OpenStruct's implementation of the id property.
68
+ # This allows us to set and retrieve id's for our corresponding
69
+ # Cashboard items.
70
+ def id; @table[:id]; end
71
+
72
+ # Returns hash of HTTP links for this object, returned as <link>
73
+ # tags in the XML.
74
+ #
75
+ # These links determine what you can do with an object, as defined
76
+ # by the Cashboard API.
77
+ def links
78
+ @links ||= begin
79
+ links = HashWithIndifferentAccess.new
80
+ self.link.each do |link|
81
+ links[link['rel']] = link['href']
82
+ end
83
+ links
84
+ end
85
+ end
86
+
87
+ # The unique HTTP URL that is used to access an object.
88
+ def href; self.links[:self]; end
89
+
90
+ # Updates the object on server, after attributes have been set.
91
+ # Returns boolean if successful
92
+ #
93
+ # Example:
94
+ # te = Cashboard::TimeEntry.new_from_url(time_entry_url)
95
+ # te.minutes = 60
96
+ # update_success = te.update
97
+ def update
98
+ options = self.class.merge_options()
99
+ options.merge!({:body => self.to_xml})
100
+ response = self.class.put(self.href, options)
101
+ begin
102
+ self.class.check_status_code(response)
103
+ rescue
104
+ return false
105
+ end
106
+ return true
107
+ end
108
+
109
+ # Destroys Cashboard object on the server.
110
+ # Returns boolean upon success.
111
+ def delete
112
+ options = self.class.merge_options()
113
+ response = self.class.delete(self.href, options)
114
+ begin
115
+ self.class.check_status_code(response)
116
+ rescue
117
+ return false
118
+ end
119
+ return true
120
+ end
121
+
122
+ # Utilizes ActiveSupport to turn our objects into XML
123
+ # that we can pass back to the server.
124
+ #
125
+ # General concept stolen from Rails CoreExtensions::Hash::Conversions
126
+ def to_xml(options={})
127
+ options[:indent] ||= 2
128
+ xml = options[:builder] ||= Builder::XmlMarkup.new(:indent => options[:indent])
129
+ xml.instruct! unless options[:skip_instruct]
130
+
131
+ obj_name = self.class.resource_name.singularize
132
+
133
+ # Turn our OpenStruct attributes into a hash we can export to XML
134
+ obj_attrs = self.marshal_dump
135
+
136
+ xml.tag!(obj_name) do
137
+ obj_attrs.each do |key,value|
138
+ next if key.to_sym == :link # Don't feed back links to server
139
+ case value
140
+ when ::Hash
141
+ value.to_xml(
142
+ options.merge({
143
+ :root => key,
144
+ :skip_instruct => true
145
+ })
146
+ )
147
+ when ::Array
148
+ value.to_xml(
149
+ options.merge({
150
+ :root => key,
151
+ :children => key.to_s.singularize,
152
+ :skip_instruct => true
153
+ })
154
+ )
155
+ else
156
+ xml.tag!(key, value)
157
+ end
158
+ end
159
+ end
160
+ end
161
+
162
+ protected
163
+ # No-configuration way to grab the resource name we're operatingo on.
164
+ # As long as we stick to a proper naming convention we have
165
+ # less shit to type out.
166
+ def self.resource_name
167
+ name.demodulize.tableize
168
+ end
169
+
170
+ # Lists a collection of things from the API and returns as
171
+ # an array of 'klass_to_return' items.
172
+ def self.get_collection(url, klass_to_return, options={})
173
+ response = get(url, merge_options(options))
174
+ check_status_code(response)
175
+ collection = response.parsed_response[klass_to_return.resource_name.singularize]
176
+ collection.map do |h|
177
+ klass_to_return.new(h)
178
+ end
179
+ end
180
+
181
+ # Ensures authentication and headers are set as options
182
+ # on each request.
183
+ def self.merge_options(options={})
184
+ options.merge!(
185
+ {
186
+ :basic_auth => @@auth,
187
+ :format => :xml,
188
+ :headers => {
189
+ 'content-type' => 'application/xml'
190
+ }
191
+ }
192
+ )
193
+ end
194
+
195
+ # Checks http status code and raises exception if it's not in
196
+ # the realm of acceptable statuses.
197
+ def self.check_status_code(response)
198
+ case response.code
199
+ when 200, 201
200
+ return # Good status codes
201
+ when 400
202
+ raise Cashboard::BadRequest.new(response)
203
+ when 401
204
+ raise Cashboard::Unauthorized.new(response)
205
+ when 402
206
+ raise Cashboard::PaymentRequired.new(response)
207
+ when 403
208
+ raise Cashboard::Forbidden.new(response)
209
+ when 404
210
+ raise Cashboard::NotFound.new(response)
211
+ when 500
212
+ raise Cashboard::ServerError.new(response)
213
+ when 502
214
+ raise Cashboard::Unavailable.new(response)
215
+ when 503
216
+ raise Cashboard::RateLimited.new(response)
217
+ else
218
+ raise Cashboard::HTTPError.new(response)
219
+ end
220
+ end
221
+
222
+ end # Cashboard::Base
223
+ end
@@ -0,0 +1,4 @@
1
+ module Cashboard
2
+ module Behaviors
3
+ end
4
+ end
@@ -0,0 +1,32 @@
1
+ # Standard interface to list associated LineItems
2
+ # for Projects and Estimates
3
+ module Cashboard::Behaviors::ListsLineItems
4
+ # Returns all associated LineItems regardless of type
5
+ def line_items(options={})
6
+ self.class.get_collection(
7
+ self.links[:line_items], Cashboard::LineItem, options
8
+ )
9
+ end
10
+
11
+ # LineItems of type 'task'
12
+ def tasks
13
+ self.filter_line_items_by :task
14
+ end
15
+
16
+ # LineItems of type 'product'
17
+ def products
18
+ self.filter_line_items_by :product
19
+ end
20
+
21
+ # LineItems of type 'custom'
22
+ def custom_items
23
+ self.filter_line_items_by :custom
24
+ end
25
+
26
+ protected
27
+ def filter_line_items_by(type)
28
+ self.line_items.reject do |li|
29
+ li.type_code != Cashboard::LineItem::TYPE_CODES[type]
30
+ end
31
+ end
32
+ end