cashboard 1.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
@@ -0,0 +1,18 @@
|
|
1
|
+
# Standard interface to toggle the status of something between Active
|
2
|
+
# and Closed inside Cashboard.
|
3
|
+
module Cashboard::Behaviors::Toggleable
|
4
|
+
# Toggles status of the project between active/closed
|
5
|
+
# and sets appropriate variable.
|
6
|
+
def toggle_status
|
7
|
+
options = self.class.merge_options()
|
8
|
+
options.merge!({:body => self.to_xml})
|
9
|
+
response = self.class.put(self.links[:toggle_status], options)
|
10
|
+
begin
|
11
|
+
self.class.check_status_code(response)
|
12
|
+
rescue
|
13
|
+
return false
|
14
|
+
end
|
15
|
+
self.is_active = !self.is_active
|
16
|
+
return true
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module Cashboard
|
2
|
+
class ClientCompany < Base
|
3
|
+
element :name
|
4
|
+
element :address
|
5
|
+
element :address2
|
6
|
+
element :city
|
7
|
+
element :state
|
8
|
+
element :zip
|
9
|
+
element :country_code
|
10
|
+
element :url
|
11
|
+
element :telephone
|
12
|
+
element :currency_type_code
|
13
|
+
element :notes
|
14
|
+
element :custom_1
|
15
|
+
element :custom_2
|
16
|
+
element :custom_3
|
17
|
+
|
18
|
+
# Returns all associated CompanyMemberships
|
19
|
+
def memberships(options={})
|
20
|
+
self.class.get_collection(
|
21
|
+
self.links[:memberships], Cashboard::CompanyMembership, options
|
22
|
+
)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module Cashboard
|
2
|
+
class ClientContact < Base
|
3
|
+
element :api_key
|
4
|
+
element :last_login, DateTime
|
5
|
+
element :login_count, Integer
|
6
|
+
element :address
|
7
|
+
element :address2
|
8
|
+
element :city
|
9
|
+
element :country_code
|
10
|
+
element :currency_type_code
|
11
|
+
element :custom_1
|
12
|
+
element :custom_2
|
13
|
+
element :custom_3
|
14
|
+
element :email_address
|
15
|
+
element :first_name
|
16
|
+
element :last_name
|
17
|
+
element :notes
|
18
|
+
element :password
|
19
|
+
element :state
|
20
|
+
element :telephone
|
21
|
+
element :url
|
22
|
+
element :zip
|
23
|
+
|
24
|
+
# Returns all associated CompanyMemberships
|
25
|
+
def memberships(options={})
|
26
|
+
self.class.get_collection(
|
27
|
+
self.links[:memberships], Cashboard::CompanyMembership, options
|
28
|
+
)
|
29
|
+
end
|
30
|
+
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module Cashboard
|
2
|
+
class Employee < Base
|
3
|
+
STATUS_CODES = {
|
4
|
+
:employee => 0,
|
5
|
+
:administrator => 2
|
6
|
+
}
|
7
|
+
|
8
|
+
element :api_key, String
|
9
|
+
element :last_login, DateTime
|
10
|
+
element :login_count, Integer
|
11
|
+
element :address
|
12
|
+
element :address2
|
13
|
+
element :city
|
14
|
+
element :country_code
|
15
|
+
element :custom_1
|
16
|
+
element :custom_2
|
17
|
+
element :custom_3
|
18
|
+
element :email_address
|
19
|
+
element :employee_status_code, Integer
|
20
|
+
element :first_name
|
21
|
+
element :last_name
|
22
|
+
element :notes
|
23
|
+
element :password
|
24
|
+
element :state
|
25
|
+
element :telephone
|
26
|
+
element :url
|
27
|
+
element :zip
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
module Cashboard
|
2
|
+
class Unauthorized < StandardError; end
|
3
|
+
|
4
|
+
class HTTPError < StandardError
|
5
|
+
attr_reader :response
|
6
|
+
def initialize(response)
|
7
|
+
@response = response
|
8
|
+
super
|
9
|
+
end
|
10
|
+
|
11
|
+
def to_s
|
12
|
+
|
13
|
+
#hint = response.response.body.nil? ? nil : response.response.body
|
14
|
+
#"#{self.class.to_s} : #{response.code}#{" - #{hint}" if hint}"
|
15
|
+
response.inspect
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
class Forbidden < HTTPError; end
|
20
|
+
class RateLimited < HTTPError; end
|
21
|
+
class NotFound < HTTPError; end
|
22
|
+
class PaymentRequired < HTTPError; end
|
23
|
+
class Unavailable < HTTPError; end
|
24
|
+
class ServerError < HTTPError; end
|
25
|
+
|
26
|
+
class BadRequest < HTTPError
|
27
|
+
# Custom parses our "errors" return XML
|
28
|
+
def to_s
|
29
|
+
response.response.body
|
30
|
+
end
|
31
|
+
|
32
|
+
# Returns a hash of errors keyed on field name.
|
33
|
+
#
|
34
|
+
# Example
|
35
|
+
# {
|
36
|
+
# :field_name_one => "Error message",
|
37
|
+
# :field_name_two => "Error message"
|
38
|
+
# }
|
39
|
+
def errors
|
40
|
+
parsed_errors = XmlSimple.xml_in(response.response.body)
|
41
|
+
error_hash = {}
|
42
|
+
parsed_errors['error'].each do |e|
|
43
|
+
error_hash[e['field']] = e['content']
|
44
|
+
end
|
45
|
+
return error_hash
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
module Cashboard
|
2
|
+
class Estimate < Base
|
3
|
+
include Cashboard::Behaviors::Toggleable
|
4
|
+
include Cashboard::Behaviors::ListsLineItems
|
5
|
+
|
6
|
+
element :assigned_id
|
7
|
+
element :agreement_text
|
8
|
+
element :client_id
|
9
|
+
element :client_type
|
10
|
+
element :created_on, DateTime
|
11
|
+
element :deposit_amount, Float
|
12
|
+
element :discount_percentage, Float
|
13
|
+
element :document_template_id
|
14
|
+
element :has_been_sent, Boolean
|
15
|
+
element :intro_text
|
16
|
+
element :is_active, Boolean
|
17
|
+
element :name
|
18
|
+
element :requires_agreement, Boolean
|
19
|
+
element :sales_tax, Float
|
20
|
+
element :sales_tax_2, Float
|
21
|
+
element :sales_tax_2_cumulative, Boolean
|
22
|
+
# Read only attributes. Can't set these via API
|
23
|
+
element :discount_best, Float
|
24
|
+
element :discount_worst, Float
|
25
|
+
element :item_actual_best, Float
|
26
|
+
element :item_actual_worst, Float
|
27
|
+
element :item_cost_best, Float
|
28
|
+
element :item_cost_worst, Float
|
29
|
+
element :item_profit_best, Float
|
30
|
+
element :item_profit_worst, Float
|
31
|
+
element :item_taxable_best, Float
|
32
|
+
element :item_taxable_worst, Float
|
33
|
+
element :price_best, Float
|
34
|
+
element :price_worst, Float
|
35
|
+
element :tax_cost_best, Float
|
36
|
+
element :tax_cost_worst, Float
|
37
|
+
element :tax_cost_2_best, Float
|
38
|
+
element :tax_cost_2_worst, Float
|
39
|
+
element :time_best, Float
|
40
|
+
element :time_worst, Float
|
41
|
+
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
module Cashboard
|
2
|
+
class Expense < Base
|
3
|
+
element :amount, Float
|
4
|
+
element :category
|
5
|
+
element :created_on, DateTime
|
6
|
+
element :description
|
7
|
+
element :invoice_line_item_id # read only
|
8
|
+
element :is_billable, Boolean
|
9
|
+
element :payee_id
|
10
|
+
element :payee_type
|
11
|
+
element :person_id
|
12
|
+
element :project_id
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,75 @@
|
|
1
|
+
module Cashboard
|
2
|
+
class Invoice < Base
|
3
|
+
element :assigned_id
|
4
|
+
element :balance, Float # readonly
|
5
|
+
element :client_id
|
6
|
+
element :client_type
|
7
|
+
element :created_on, DateTime
|
8
|
+
element :discount, Float # readonly
|
9
|
+
element :discount_percentage, Float
|
10
|
+
element :document_template_id
|
11
|
+
element :due_date, Date
|
12
|
+
element :early_period_in_days, Integer
|
13
|
+
element :has_been_sent, Boolean
|
14
|
+
element :include_expenses, Boolean
|
15
|
+
element :include_pdf, Boolean
|
16
|
+
element :include_time_entries, Boolean
|
17
|
+
element :invoice_date, Date
|
18
|
+
element :item_actual, Float # readonly
|
19
|
+
element :item_cost, Float # readonly
|
20
|
+
element :item_profit, Float # readonly
|
21
|
+
element :item_taxable, Float # readonly
|
22
|
+
element :late_fee, Float # readonly
|
23
|
+
element :late_percentage, Float
|
24
|
+
element :late_period_in_days, Integer
|
25
|
+
element :notes
|
26
|
+
element :payment_total, Float # readonly
|
27
|
+
element :post_reminder_in_days, Integer
|
28
|
+
element :pre_reminder_in_days, Integer
|
29
|
+
element :sales_tax, Float
|
30
|
+
element :sales_tax_2, Float
|
31
|
+
element :sales_tax_2_cumulative, Boolean
|
32
|
+
element :total, Float # readonly
|
33
|
+
element :total_quantity, Float # readonly
|
34
|
+
|
35
|
+
# Returns all associated LineItems
|
36
|
+
def line_items(options={})
|
37
|
+
self.class.get_collection(
|
38
|
+
self.links[:line_items], Cashboard::InvoiceLineItem, options
|
39
|
+
)
|
40
|
+
end
|
41
|
+
|
42
|
+
# Imports uninvoiced items (time entries, expenses, flat fee tasks)
|
43
|
+
# that belong to the same client that this invoice was created for.
|
44
|
+
#
|
45
|
+
# Either raises a Cashboard error (errors.rb) or returns a collection
|
46
|
+
# of Cashboard::InvoiceLineItem objects.
|
47
|
+
def import_uninvoiced_items(project_ids={}, start_date=nil, end_date=nil)
|
48
|
+
xml_options = get_import_xml_options(project_ids, start_date, end_date)
|
49
|
+
|
50
|
+
options = self.class.merge_options()
|
51
|
+
options.merge!({:body => xml_options})
|
52
|
+
response = self.class.put(self.links[:import_uninvoiced_items], options)
|
53
|
+
|
54
|
+
self.class.check_status_code(response)
|
55
|
+
|
56
|
+
collection = response.parsed_response[Cashboard::InvoiceLineItem.resource_name.singularize]
|
57
|
+
collection.map do |h|
|
58
|
+
Cashboard::InvoiceLineItem.new(h)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def get_import_xml_options(project_ids, start_date, end_date)
|
63
|
+
xml_options = ''
|
64
|
+
xml = Builder::XmlMarkup.new(:target => xml_options, :indent => 2)
|
65
|
+
xml.instruct!
|
66
|
+
xml.projects do
|
67
|
+
project_ids.each {|pid| xml.id pid} if project_ids
|
68
|
+
end
|
69
|
+
xml.start_date start_date
|
70
|
+
xml.end_date end_date
|
71
|
+
|
72
|
+
return xml_options
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module Cashboard
|
2
|
+
class InvoiceLineItem < Base
|
3
|
+
element :description
|
4
|
+
element :flat_fee, Float
|
5
|
+
element :invoice_id
|
6
|
+
element :invoice_schedule_id
|
7
|
+
element :is_taxable, Boolean
|
8
|
+
element :markup_percentage, Float
|
9
|
+
element :price_per, Float
|
10
|
+
element :quantity, Float
|
11
|
+
element :rank, Integer
|
12
|
+
element :title
|
13
|
+
element :total, Float # readonly
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module Cashboard
|
2
|
+
class LineItem < Base
|
3
|
+
TYPE_CODES = {
|
4
|
+
:custom => 0,
|
5
|
+
:task => 1,
|
6
|
+
:product => 2
|
7
|
+
}
|
8
|
+
|
9
|
+
element :best_time_in_minutes, Integer
|
10
|
+
element :created_on, DateTime
|
11
|
+
element :description, String
|
12
|
+
element :estimate_id, String
|
13
|
+
element :flat_fee, Float
|
14
|
+
element :is_complete, Boolean
|
15
|
+
element :is_taxable, Boolean
|
16
|
+
element :markup_percentage, Float
|
17
|
+
element :price_per, Float
|
18
|
+
element :project_id, String
|
19
|
+
element :quantity_low, Float
|
20
|
+
element :quantity_high, Float
|
21
|
+
element :rank, Integer
|
22
|
+
element :title, String
|
23
|
+
element :type_code, Integer
|
24
|
+
element :unit_label, String
|
25
|
+
element :worst_time_in_minutes, Integer
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module Cashboard
|
2
|
+
class Payment < Base
|
3
|
+
element :amount, Float
|
4
|
+
element :assigned_id
|
5
|
+
element :client_id
|
6
|
+
element :client_type
|
7
|
+
element :created_on, Date
|
8
|
+
element :document_template_id
|
9
|
+
element :estimate_id
|
10
|
+
element :notes
|
11
|
+
element :person_id
|
12
|
+
element :transaction_id
|
13
|
+
|
14
|
+
# Returns all InvoicePayments associated with this payment
|
15
|
+
def invoice_payments(options={})
|
16
|
+
self.class.get_collection(
|
17
|
+
self.links[:invoices], Cashboard::InvoicePayment, options
|
18
|
+
)
|
19
|
+
end
|
20
|
+
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
module Cashboard
|
2
|
+
class Project < Base
|
3
|
+
include Cashboard::Behaviors::Toggleable
|
4
|
+
include Cashboard::Behaviors::ListsLineItems
|
5
|
+
|
6
|
+
BILLING_CODES = {
|
7
|
+
:non_billable => 0,
|
8
|
+
:task_rate => 1,
|
9
|
+
:employee_rate => 2
|
10
|
+
}
|
11
|
+
|
12
|
+
CLIENT_VIEW_TIME_CODES = {
|
13
|
+
:show_when_invoiced => 0,
|
14
|
+
:show_when_marked_billable => 1,
|
15
|
+
:never => 2
|
16
|
+
}
|
17
|
+
|
18
|
+
element :billing_code, Integer
|
19
|
+
element :client_name, String
|
20
|
+
element :client_id, Integer
|
21
|
+
element :client_type, String
|
22
|
+
element :client_view_time_code, Integer
|
23
|
+
element :completion_date, Date
|
24
|
+
element :created_on, Date
|
25
|
+
element :is_active, Boolean
|
26
|
+
element :name, String
|
27
|
+
element :rate, Float
|
28
|
+
element :start_date, Date
|
29
|
+
|
30
|
+
# Returns all employee ProjectAssignments
|
31
|
+
def employee_project_assignments(options={})
|
32
|
+
self.class.get_collection(
|
33
|
+
self.links[:assigned_employees], Cashboard::ProjectAssignment, options
|
34
|
+
)
|
35
|
+
end
|
36
|
+
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
module Cashboard
|
2
|
+
class TimeEntry < Base
|
3
|
+
element :created_on, DateTime
|
4
|
+
element :description
|
5
|
+
element :invoice_line_item_id # readonly
|
6
|
+
element :is_billable, Boolean
|
7
|
+
element :is_running, Boolean # readonly
|
8
|
+
element :line_item_id
|
9
|
+
element :minutes, Integer
|
10
|
+
element :minutes_with_timer, Integer # readonly
|
11
|
+
element :person_id
|
12
|
+
element :timer_started_at, DateTime # readonly
|
13
|
+
|
14
|
+
# Starts or stops timer depending on its current state.
|
15
|
+
#
|
16
|
+
# Will return an object of Cashboard::Struct if another timer was stopped
|
17
|
+
# during this toggle operation.
|
18
|
+
#
|
19
|
+
# Will return nil if no timer was stopped.
|
20
|
+
def toggle_timer
|
21
|
+
options = self.class.merge_options()
|
22
|
+
options.merge!({:body => self.to_xml})
|
23
|
+
response = self.class.put(self.links[:toggle_timer], options)
|
24
|
+
|
25
|
+
# Raise special errors if not a success
|
26
|
+
self.class.check_status_code(response)
|
27
|
+
|
28
|
+
# Re-initialize ourselves with information from response
|
29
|
+
initialize(response.parsed_response)
|
30
|
+
|
31
|
+
if self.stopped_timer
|
32
|
+
stopped_timer = Cashboard::Struct.new(self.stopped_timer)
|
33
|
+
end
|
34
|
+
|
35
|
+
stopped_timer || nil
|
36
|
+
end
|
37
|
+
|
38
|
+
# If a TimeEntry has no invoice_line_item_id set, then it
|
39
|
+
# hasn't been included on an invoice.
|
40
|
+
def has_been_invoiced?
|
41
|
+
!self.invoice_line_item_id.blank?
|
42
|
+
end
|
43
|
+
|
44
|
+
end
|
45
|
+
end
|
data/lib/cashboard.rb
ADDED
@@ -0,0 +1,75 @@
|
|
1
|
+
$LOAD_PATH << File.join(File.dirname(__FILE__))
|
2
|
+
|
3
|
+
require 'typecasted_open_struct'
|
4
|
+
require 'rubygems'
|
5
|
+
require 'active_support'
|
6
|
+
require 'httparty'
|
7
|
+
require 'xmlsimple'
|
8
|
+
require 'builder'
|
9
|
+
|
10
|
+
module Cashboard
|
11
|
+
# When reading the parsed hashes generated from parser we ignore these pairs.
|
12
|
+
IGNORED_XML_KEYS = ['rel', 'read_only']
|
13
|
+
|
14
|
+
class Struct < TypecastedOpenStruct
|
15
|
+
# Since we're dealing with initializing from hashes with 'content'
|
16
|
+
# keys we need to set properties based on those keys.
|
17
|
+
#
|
18
|
+
# Additionally, we do some magic to ignore attributes we don't care about.
|
19
|
+
#
|
20
|
+
# The basic concept is lifted from ostruct.rb
|
21
|
+
def initialize(hash={})
|
22
|
+
@table = {}
|
23
|
+
return unless hash
|
24
|
+
hash.each do |k,v|
|
25
|
+
# Remove keys that aren't useful for our purposes.
|
26
|
+
if v.class == Hash
|
27
|
+
Cashboard::IGNORED_XML_KEYS.each {|ignored| v.delete(ignored)}
|
28
|
+
end
|
29
|
+
# Access items based on the 'content' key inside the hash.
|
30
|
+
# Allows us to deal with all XML tags equally, even if the tags
|
31
|
+
# have attributes or not.
|
32
|
+
if v.class == Hash && v['content']
|
33
|
+
@table[k.to_sym] = v['content']
|
34
|
+
elsif v.class == Hash && v.empty?
|
35
|
+
@table[k.to_sym] = nil
|
36
|
+
else
|
37
|
+
@table[k.to_sym] = v
|
38
|
+
end
|
39
|
+
new_ostruct_member(k)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
# Override HTTParty's XML parsing, which doesn't really work
|
46
|
+
# well for the output we receive.
|
47
|
+
class HTTParty::Parser
|
48
|
+
protected
|
49
|
+
def xml
|
50
|
+
XmlSimple.xml_in(
|
51
|
+
body,
|
52
|
+
'KeepRoot' => false,
|
53
|
+
# Force 'link' tags into an array always
|
54
|
+
'ForceArray' => %w(link),
|
55
|
+
# Force each item into a hash with a 'content' key for the tag value
|
56
|
+
# If we don't do this random tag attributes can screw us up.
|
57
|
+
'ForceContent' => true
|
58
|
+
)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
# After we've defined some basics let's include
|
63
|
+
# the Cashboard-rb API libraries
|
64
|
+
|
65
|
+
# Load base first or there's some issues with dependencies.
|
66
|
+
require 'cashboard/base'
|
67
|
+
require 'cashboard/behaviors/base'
|
68
|
+
require 'cashboard/behaviors/toggleable'
|
69
|
+
require 'cashboard/behaviors/lists_line_items'
|
70
|
+
|
71
|
+
library_files = Dir[File.join(File.dirname(__FILE__), 'cashboard/*.rb')]
|
72
|
+
library_files.each do |lib|
|
73
|
+
next if lib.include?('cashboard/base.rb')
|
74
|
+
require lib
|
75
|
+
end
|
@@ -0,0 +1,82 @@
|
|
1
|
+
require 'ostruct'
|
2
|
+
require 'date'
|
3
|
+
require 'time'
|
4
|
+
|
5
|
+
class Boolean; end
|
6
|
+
|
7
|
+
# A class to provide a quick and dirty way to typecast attributes
|
8
|
+
# that we specify, while silently setting the rest.
|
9
|
+
#
|
10
|
+
# Allows us to specify important attributes, but doesn't force us to
|
11
|
+
# update schema in order to deal with unexpected new properties.
|
12
|
+
#
|
13
|
+
#
|
14
|
+
# Example:
|
15
|
+
#
|
16
|
+
# class MyFoo < TypecastedOpenStruct
|
17
|
+
# element :true_false_thing, Boolean
|
18
|
+
# element :amount, Float
|
19
|
+
# end
|
20
|
+
class TypecastedOpenStruct < OpenStruct
|
21
|
+
@@elements = {}
|
22
|
+
|
23
|
+
def self.element(name, attr_type=String, options={})
|
24
|
+
element = Element.new(name, attr_type, options)
|
25
|
+
@@elements[name] = element
|
26
|
+
|
27
|
+
# Define getter to attr_typecast proper value
|
28
|
+
define_method(element.method_name) do
|
29
|
+
self.class.attr_typecast(
|
30
|
+
@table[element.method_name.to_sym],
|
31
|
+
@@elements[name].attr_type
|
32
|
+
)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
# Lifted from HappyMapper. Thanks! :)
|
37
|
+
def self.attr_typecast(value, attr_type)
|
38
|
+
return value if value.kind_of?(attr_type) || value.nil?
|
39
|
+
begin
|
40
|
+
if attr_type == String then value.to_s
|
41
|
+
elsif attr_type == Float then value.to_f
|
42
|
+
elsif attr_type == Time then Time.parse(value.to_s)
|
43
|
+
elsif attr_type == Date then Date.parse(value.to_s)
|
44
|
+
elsif attr_type == DateTime then DateTime.parse(value.to_s)
|
45
|
+
elsif attr_type == Boolean then ['true', 't', '1'].include?(value.to_s.downcase)
|
46
|
+
elsif attr_type == Integer
|
47
|
+
# ganked from datamapper
|
48
|
+
value_to_i = value.to_i
|
49
|
+
if value_to_i == 0 && value != '0'
|
50
|
+
value_to_s = value.to_s
|
51
|
+
begin
|
52
|
+
Integer(value_to_s =~ /^(\d+)/ ? $1 : value_to_s)
|
53
|
+
rescue ArgumentError
|
54
|
+
nil
|
55
|
+
end
|
56
|
+
else
|
57
|
+
value_to_i
|
58
|
+
end
|
59
|
+
else
|
60
|
+
value
|
61
|
+
end
|
62
|
+
rescue
|
63
|
+
value
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
class Element
|
68
|
+
attr_types = [String, Float, Time, Date, DateTime, Integer, Boolean]
|
69
|
+
|
70
|
+
attr_accessor :name, :attr_type, :options, :namespace
|
71
|
+
|
72
|
+
def initialize(name, attr_type=String, options={})
|
73
|
+
self.name = name.to_s
|
74
|
+
self.attr_type = attr_type
|
75
|
+
self.options = options
|
76
|
+
end
|
77
|
+
|
78
|
+
def method_name
|
79
|
+
@method_name ||= name.tr('-', '_')
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|