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.
- data/.autotest +26 -0
- data/.gitignore +2 -0
- data/LICENSE +19 -0
- data/README.textile +58 -0
- data/Rakefile +25 -0
- data/cashboard.gemspec +129 -0
- data/examples/create_account.rb +38 -0
- data/examples/list_stuff_in_account.rb +15 -0
- data/examples/simple_workflow.rb +35 -0
- data/examples/time_tracking.rb +30 -0
- data/examples/toggle_timer.rb +0 -0
- data/lib/cashboard/account.rb +21 -0
- data/lib/cashboard/base.rb +223 -0
- data/lib/cashboard/behaviors/base.rb +4 -0
- data/lib/cashboard/behaviors/lists_line_items.rb +32 -0
- data/lib/cashboard/behaviors/toggleable.rb +18 -0
- data/lib/cashboard/client_company.rb +25 -0
- data/lib/cashboard/client_contact.rb +32 -0
- data/lib/cashboard/company_membership.rb +6 -0
- data/lib/cashboard/document_template.rb +10 -0
- data/lib/cashboard/employee.rb +29 -0
- data/lib/cashboard/errors.rb +48 -0
- data/lib/cashboard/estimate.rb +43 -0
- data/lib/cashboard/expense.rb +14 -0
- data/lib/cashboard/invoice.rb +75 -0
- data/lib/cashboard/invoice_line_item.rb +15 -0
- data/lib/cashboard/invoice_payment.rb +7 -0
- data/lib/cashboard/line_item.rb +27 -0
- data/lib/cashboard/payment.rb +22 -0
- data/lib/cashboard/project.rb +38 -0
- data/lib/cashboard/project_assignment.rb +9 -0
- data/lib/cashboard/time_entry.rb +45 -0
- data/lib/cashboard/version.rb +3 -0
- data/lib/cashboard.rb +75 -0
- data/lib/typecasted_open_struct.rb +82 -0
- data/test/fixtures/account.xml +50 -0
- data/test/fixtures/cashboard_credentials.yml +3 -0
- data/test/fixtures/client_companies.xml +41 -0
- data/test/fixtures/client_contacts.xml +53 -0
- data/test/fixtures/company_memberships.xml +21 -0
- data/test/fixtures/document_templates.xml +53 -0
- data/test/fixtures/employees.xml +51 -0
- data/test/fixtures/estimates.xml +243 -0
- data/test/fixtures/expenses.xml +101 -0
- data/test/fixtures/invoice_line_items.xml +138 -0
- data/test/fixtures/invoice_payments.xml +10 -0
- data/test/fixtures/invoices.xml +231 -0
- data/test/fixtures/line_items.xml +243 -0
- data/test/fixtures/payments.xml +93 -0
- data/test/fixtures/project_assignments.xml +30 -0
- data/test/fixtures/projects.xml +129 -0
- data/test/fixtures/time_entries.xml +213 -0
- data/test/full.rb +3 -0
- data/test/test_helper.rb +112 -0
- data/test/unit/account_test.rb +85 -0
- data/test/unit/document_template_test.rb +18 -0
- data/test/unit/estimate_test.rb +166 -0
- data/test/unit/expense_test.rb +16 -0
- data/test/unit/invoice_test.rb +185 -0
- data/test/unit/project_test.rb +198 -0
- data/test/unit/time_entry_test.rb +225 -0
- 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
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,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
|