invoicing 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG +3 -0
- data/LICENSE +20 -0
- data/Manifest +60 -0
- data/README +48 -0
- data/Rakefile +75 -0
- data/invoicing.gemspec +41 -0
- data/lib/invoicing.rb +9 -0
- data/lib/invoicing/cached_record.rb +107 -0
- data/lib/invoicing/class_info.rb +187 -0
- data/lib/invoicing/connection_adapter_ext.rb +44 -0
- data/lib/invoicing/countries/uk.rb +24 -0
- data/lib/invoicing/currency_value.rb +212 -0
- data/lib/invoicing/find_subclasses.rb +193 -0
- data/lib/invoicing/ledger_item.rb +718 -0
- data/lib/invoicing/ledger_item/render_html.rb +515 -0
- data/lib/invoicing/ledger_item/render_ubl.rb +268 -0
- data/lib/invoicing/line_item.rb +246 -0
- data/lib/invoicing/price.rb +9 -0
- data/lib/invoicing/tax_rate.rb +9 -0
- data/lib/invoicing/taxable.rb +355 -0
- data/lib/invoicing/time_dependent.rb +388 -0
- data/lib/invoicing/version.rb +21 -0
- data/test/cached_record_test.rb +100 -0
- data/test/class_info_test.rb +253 -0
- data/test/connection_adapter_ext_test.rb +71 -0
- data/test/currency_value_test.rb +184 -0
- data/test/find_subclasses_test.rb +120 -0
- data/test/fixtures/README +7 -0
- data/test/fixtures/cached_record.sql +22 -0
- data/test/fixtures/class_info.sql +28 -0
- data/test/fixtures/currency_value.sql +29 -0
- data/test/fixtures/find_subclasses.sql +43 -0
- data/test/fixtures/ledger_item.sql +39 -0
- data/test/fixtures/line_item.sql +33 -0
- data/test/fixtures/price.sql +4 -0
- data/test/fixtures/tax_rate.sql +4 -0
- data/test/fixtures/taxable.sql +14 -0
- data/test/fixtures/time_dependent.sql +35 -0
- data/test/ledger_item_test.rb +352 -0
- data/test/line_item_test.rb +139 -0
- data/test/models/README +4 -0
- data/test/models/test_subclass_in_another_file.rb +3 -0
- data/test/models/test_subclass_not_in_database.rb +6 -0
- data/test/price_test.rb +9 -0
- data/test/ref-output/creditnote3.html +82 -0
- data/test/ref-output/creditnote3.xml +89 -0
- data/test/ref-output/invoice1.html +93 -0
- data/test/ref-output/invoice1.xml +111 -0
- data/test/ref-output/invoice2.html +86 -0
- data/test/ref-output/invoice2.xml +98 -0
- data/test/ref-output/invoice_null.html +36 -0
- data/test/render_html_test.rb +69 -0
- data/test/render_ubl_test.rb +32 -0
- data/test/setup.rb +37 -0
- data/test/tax_rate_test.rb +9 -0
- data/test/taxable_test.rb +180 -0
- data/test/test_helper.rb +48 -0
- data/test/time_dependent_test.rb +180 -0
- data/website/curvycorners.js +1 -0
- data/website/screen.css +149 -0
- data/website/template.html.erb +43 -0
- metadata +180 -0
data/CHANGELOG
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2009 Martin Kleppmann
|
2
|
+
Copyright (c) 2009 Ept Computing Limited
|
3
|
+
|
4
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
5
|
+
this software and associated documentation files (the "Software"), to deal in
|
6
|
+
the Software without restriction, including without limitation the rights to
|
7
|
+
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
8
|
+
of the Software, and to permit persons to whom the Software is furnished to do
|
9
|
+
so, subject to the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be included in all
|
12
|
+
copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
15
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
16
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
17
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
18
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
19
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
20
|
+
SOFTWARE.
|
data/Manifest
ADDED
@@ -0,0 +1,60 @@
|
|
1
|
+
CHANGELOG
|
2
|
+
lib/invoicing/cached_record.rb
|
3
|
+
lib/invoicing/class_info.rb
|
4
|
+
lib/invoicing/connection_adapter_ext.rb
|
5
|
+
lib/invoicing/countries/uk.rb
|
6
|
+
lib/invoicing/currency_value.rb
|
7
|
+
lib/invoicing/find_subclasses.rb
|
8
|
+
lib/invoicing/ledger_item/render_html.rb
|
9
|
+
lib/invoicing/ledger_item/render_ubl.rb
|
10
|
+
lib/invoicing/ledger_item.rb
|
11
|
+
lib/invoicing/line_item.rb
|
12
|
+
lib/invoicing/price.rb
|
13
|
+
lib/invoicing/tax_rate.rb
|
14
|
+
lib/invoicing/taxable.rb
|
15
|
+
lib/invoicing/time_dependent.rb
|
16
|
+
lib/invoicing/version.rb
|
17
|
+
lib/invoicing.rb
|
18
|
+
LICENSE
|
19
|
+
Manifest
|
20
|
+
Rakefile
|
21
|
+
README
|
22
|
+
test/cached_record_test.rb
|
23
|
+
test/class_info_test.rb
|
24
|
+
test/connection_adapter_ext_test.rb
|
25
|
+
test/currency_value_test.rb
|
26
|
+
test/find_subclasses_test.rb
|
27
|
+
test/fixtures/cached_record.sql
|
28
|
+
test/fixtures/class_info.sql
|
29
|
+
test/fixtures/currency_value.sql
|
30
|
+
test/fixtures/find_subclasses.sql
|
31
|
+
test/fixtures/ledger_item.sql
|
32
|
+
test/fixtures/line_item.sql
|
33
|
+
test/fixtures/price.sql
|
34
|
+
test/fixtures/README
|
35
|
+
test/fixtures/tax_rate.sql
|
36
|
+
test/fixtures/taxable.sql
|
37
|
+
test/fixtures/time_dependent.sql
|
38
|
+
test/ledger_item_test.rb
|
39
|
+
test/line_item_test.rb
|
40
|
+
test/models/README
|
41
|
+
test/models/test_subclass_in_another_file.rb
|
42
|
+
test/models/test_subclass_not_in_database.rb
|
43
|
+
test/price_test.rb
|
44
|
+
test/ref-output/creditnote3.html
|
45
|
+
test/ref-output/creditnote3.xml
|
46
|
+
test/ref-output/invoice1.html
|
47
|
+
test/ref-output/invoice1.xml
|
48
|
+
test/ref-output/invoice2.html
|
49
|
+
test/ref-output/invoice2.xml
|
50
|
+
test/ref-output/invoice_null.html
|
51
|
+
test/render_html_test.rb
|
52
|
+
test/render_ubl_test.rb
|
53
|
+
test/setup.rb
|
54
|
+
test/tax_rate_test.rb
|
55
|
+
test/taxable_test.rb
|
56
|
+
test/test_helper.rb
|
57
|
+
test/time_dependent_test.rb
|
58
|
+
website/curvycorners.js
|
59
|
+
website/screen.css
|
60
|
+
website/template.html.erb
|
data/README
ADDED
@@ -0,0 +1,48 @@
|
|
1
|
+
h1. Ruby invoicing framework
|
2
|
+
|
3
|
+
* "Homepage":http://invoicing.rubyforge.org/
|
4
|
+
* "API Reference Docs":http://invoicing.rubyforge.org/docs/invoicing/
|
5
|
+
* "RubyForge project":http://rubyforge.org/projects/invoicing/
|
6
|
+
* Email: Martin Kleppmann <ept@rubyforge.org>
|
7
|
+
|
8
|
+
h2. DESCRIPTION
|
9
|
+
|
10
|
+
Framework for generating and displaying invoices (ideal for commercial Rails
|
11
|
+
apps). Allows for flexible business logic; provides tools for tax handling,
|
12
|
+
commission calculation etc. Both developer-friendly and accountant-friendly.
|
13
|
+
|
14
|
+
The Ruby invoicing framework provides tools, helpers and a structure for
|
15
|
+
applications (particularly web apps) which need to generate invoices for
|
16
|
+
customers. It builds on ActiveRecord and is particularly suited for Rails
|
17
|
+
applications, but could be used with other frameworks too.
|
18
|
+
|
19
|
+
h2. FEATURES
|
20
|
+
|
21
|
+
* Dealing with tax rates changing over time
|
22
|
+
* Calculating commissions (e.g. in a reseller setting)
|
23
|
+
|
24
|
+
h2. SYNOPSIS
|
25
|
+
|
26
|
+
<pre syntax="ruby">
|
27
|
+
require 'invoicing'
|
28
|
+
</pre>
|
29
|
+
|
30
|
+
h2. STATUS
|
31
|
+
|
32
|
+
So far, the Ruby Invoicing Framework has been tested with ActiveRecord 2.2.2,
|
33
|
+
MySQL 5.0.67 and PostgreSQL 8.3.5. We will be testing it across a wider
|
34
|
+
variety of versions soon.
|
35
|
+
|
36
|
+
h2. CREDITS
|
37
|
+
|
38
|
+
The Ruby invoicing framework originated as part of the website Bid for Wine
|
39
|
+
(http://www.bidforwine.co.uk), developed by Patrick Dietrich, Conrad Irwin
|
40
|
+
and Michael Arnold for Ept Computing Ltd. It was extracted from the Bid for
|
41
|
+
Wine codebase and substantially extended by Martin Kleppmann.
|
42
|
+
|
43
|
+
h2. LICENSE
|
44
|
+
|
45
|
+
Copyright (c) 2009 Martin Kleppmann, Ept Computing Limited.
|
46
|
+
|
47
|
+
This gem is made publicly available under the terms of the MIT license.
|
48
|
+
See LICENSE and/or COPYING for details.
|
data/Rakefile
ADDED
@@ -0,0 +1,75 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'echoe'
|
3
|
+
|
4
|
+
# Add the project's top level directory and the lib directory to the Ruby search path
|
5
|
+
$: << File.expand_path(File.join(File.dirname(__FILE__), "lib"))
|
6
|
+
$: << File.expand_path(File.dirname(__FILE__))
|
7
|
+
|
8
|
+
require 'invoicing'
|
9
|
+
|
10
|
+
Echoe.new('invoicing', Invoicing::VERSION) do |p|
|
11
|
+
p.summary = 'Ruby invoicing framework'
|
12
|
+
p.description = 'Provides tools for applications which need to generate invoices for customers.'
|
13
|
+
p.url = 'http://invoicing.rubyforge.org/'
|
14
|
+
p.author = 'Martin Kleppmann'
|
15
|
+
p.email = 'rubyforge@eptcomputing.com'
|
16
|
+
p.dependencies = ['activerecord >=2.1.0', 'builder >= 2.0']
|
17
|
+
p.docs_host = 'ept@rubyforge.org:/var/www/gforge-projects/'
|
18
|
+
p.test_pattern = 'test/*_test.rb' # do not include test/models/*.rb
|
19
|
+
p.rcov_options = "-x '/Library/'"
|
20
|
+
end
|
21
|
+
|
22
|
+
|
23
|
+
desc "Generate a new website from README file"
|
24
|
+
task 'website' do
|
25
|
+
require 'rubygems'
|
26
|
+
require 'redcloth'
|
27
|
+
require 'syntax/convertors/html'
|
28
|
+
require 'erb'
|
29
|
+
|
30
|
+
version = Invoicing::VERSION
|
31
|
+
download = 'http://rubyforge.org/projects/invoicing'
|
32
|
+
|
33
|
+
def convert_syntax(syntax, source)
|
34
|
+
return Syntax::Convertors::HTML.for_syntax(syntax).convert(source).gsub(%r!^<pre>|</pre>$!,'')
|
35
|
+
end
|
36
|
+
|
37
|
+
template = ERB.new(File.open(File.join(File.dirname(__FILE__), '/website/template.html.erb')).read)
|
38
|
+
|
39
|
+
title = nil
|
40
|
+
body = nil
|
41
|
+
File.open(File.join(File.dirname(__FILE__), '/README')) do |fsrc|
|
42
|
+
title = fsrc.readline.gsub(/^[^ ]* /, '')
|
43
|
+
body_text = fsrc.read
|
44
|
+
syntax_items = []
|
45
|
+
body_text.gsub!(%r!<(pre|code)[^>]*?syntax=['"]([^'"]+)[^>]*>(.*?)</\1>!m){
|
46
|
+
ident = syntax_items.length
|
47
|
+
element, syntax, source = $1, $2, $3
|
48
|
+
syntax_items << "<#{element} class='syntax'>#{convert_syntax(syntax, source)}</#{element}>"
|
49
|
+
"syntax-temp-#{ident}"
|
50
|
+
}
|
51
|
+
body = RedCloth.new(body_text).to_html
|
52
|
+
body.gsub!(%r!(?:<pre><code>)?syntax-temp-(\d+)(?:</code></pre>)?!){ syntax_items[$1.to_i] }
|
53
|
+
end
|
54
|
+
|
55
|
+
File.open(File.join(File.dirname(__FILE__), '/website/index.html'), 'w') do |fout|
|
56
|
+
fout.write(template.result(binding))
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
|
61
|
+
desc "Generate and publish website"
|
62
|
+
task :publish_website => :website do
|
63
|
+
require 'net/sftp'
|
64
|
+
upload_user = 'ept'
|
65
|
+
upload_host = 'rubyforge.org'
|
66
|
+
upload_dir = '/var/www/gforge-projects/invoicing'
|
67
|
+
local_dir = File.join(File.dirname(__FILE__), 'website')
|
68
|
+
Net::SFTP.start(upload_host, upload_user) do |sftp|
|
69
|
+
for f in Dir.entries(local_dir)
|
70
|
+
next if f =~ /^\./
|
71
|
+
puts "Uploading #{f} to #{upload_user}@#{upload_host}:#{upload_dir}/#{f}"
|
72
|
+
sftp.upload!(File.join(local_dir, f), "#{upload_dir}/#{f}")
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
data/invoicing.gemspec
ADDED
@@ -0,0 +1,41 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
|
3
|
+
Gem::Specification.new do |s|
|
4
|
+
s.name = %q{invoicing}
|
5
|
+
s.version = "0.1.0"
|
6
|
+
|
7
|
+
s.required_rubygems_version = Gem::Requirement.new(">= 1.2") if s.respond_to? :required_rubygems_version=
|
8
|
+
s.authors = ["Martin Kleppmann"]
|
9
|
+
s.date = %q{2009-02-10}
|
10
|
+
s.description = %q{Provides tools for applications which need to generate invoices for customers.}
|
11
|
+
s.email = %q{rubyforge@eptcomputing.com}
|
12
|
+
s.extra_rdoc_files = ["CHANGELOG", "lib/invoicing/cached_record.rb", "lib/invoicing/class_info.rb", "lib/invoicing/connection_adapter_ext.rb", "lib/invoicing/countries/uk.rb", "lib/invoicing/currency_value.rb", "lib/invoicing/find_subclasses.rb", "lib/invoicing/ledger_item/render_html.rb", "lib/invoicing/ledger_item/render_ubl.rb", "lib/invoicing/ledger_item.rb", "lib/invoicing/line_item.rb", "lib/invoicing/price.rb", "lib/invoicing/tax_rate.rb", "lib/invoicing/taxable.rb", "lib/invoicing/time_dependent.rb", "lib/invoicing/version.rb", "lib/invoicing.rb", "LICENSE", "README"]
|
13
|
+
s.files = ["CHANGELOG", "lib/invoicing/cached_record.rb", "lib/invoicing/class_info.rb", "lib/invoicing/connection_adapter_ext.rb", "lib/invoicing/countries/uk.rb", "lib/invoicing/currency_value.rb", "lib/invoicing/find_subclasses.rb", "lib/invoicing/ledger_item/render_html.rb", "lib/invoicing/ledger_item/render_ubl.rb", "lib/invoicing/ledger_item.rb", "lib/invoicing/line_item.rb", "lib/invoicing/price.rb", "lib/invoicing/tax_rate.rb", "lib/invoicing/taxable.rb", "lib/invoicing/time_dependent.rb", "lib/invoicing/version.rb", "lib/invoicing.rb", "LICENSE", "Manifest", "Rakefile", "README", "test/cached_record_test.rb", "test/class_info_test.rb", "test/connection_adapter_ext_test.rb", "test/currency_value_test.rb", "test/find_subclasses_test.rb", "test/fixtures/cached_record.sql", "test/fixtures/class_info.sql", "test/fixtures/currency_value.sql", "test/fixtures/find_subclasses.sql", "test/fixtures/ledger_item.sql", "test/fixtures/line_item.sql", "test/fixtures/price.sql", "test/fixtures/README", "test/fixtures/tax_rate.sql", "test/fixtures/taxable.sql", "test/fixtures/time_dependent.sql", "test/ledger_item_test.rb", "test/line_item_test.rb", "test/models/README", "test/models/test_subclass_in_another_file.rb", "test/models/test_subclass_not_in_database.rb", "test/price_test.rb", "test/ref-output/creditnote3.html", "test/ref-output/creditnote3.xml", "test/ref-output/invoice1.html", "test/ref-output/invoice1.xml", "test/ref-output/invoice2.html", "test/ref-output/invoice2.xml", "test/ref-output/invoice_null.html", "test/render_html_test.rb", "test/render_ubl_test.rb", "test/setup.rb", "test/tax_rate_test.rb", "test/taxable_test.rb", "test/test_helper.rb", "test/time_dependent_test.rb", "website/curvycorners.js", "website/screen.css", "website/template.html.erb", "invoicing.gemspec"]
|
14
|
+
s.has_rdoc = true
|
15
|
+
s.homepage = %q{http://invoicing.rubyforge.org/}
|
16
|
+
s.rdoc_options = ["--line-numbers", "--inline-source", "--title", "Invoicing", "--main", "README"]
|
17
|
+
s.require_paths = ["lib"]
|
18
|
+
s.rubyforge_project = %q{invoicing}
|
19
|
+
s.rubygems_version = %q{1.3.1}
|
20
|
+
s.summary = %q{Ruby invoicing framework}
|
21
|
+
s.test_files = ["test/cached_record_test.rb", "test/class_info_test.rb", "test/connection_adapter_ext_test.rb", "test/currency_value_test.rb", "test/find_subclasses_test.rb", "test/ledger_item_test.rb", "test/line_item_test.rb", "test/price_test.rb", "test/render_html_test.rb", "test/render_ubl_test.rb", "test/tax_rate_test.rb", "test/taxable_test.rb", "test/time_dependent_test.rb"]
|
22
|
+
|
23
|
+
if s.respond_to? :specification_version then
|
24
|
+
current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
|
25
|
+
s.specification_version = 2
|
26
|
+
|
27
|
+
if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
|
28
|
+
s.add_runtime_dependency(%q<activerecord>, [">= 2.1.0"])
|
29
|
+
s.add_runtime_dependency(%q<builder>, [">= 0", "= 2.0"])
|
30
|
+
s.add_development_dependency(%q<echoe>, [">= 0"])
|
31
|
+
else
|
32
|
+
s.add_dependency(%q<activerecord>, [">= 2.1.0"])
|
33
|
+
s.add_dependency(%q<builder>, [">= 0", "= 2.0"])
|
34
|
+
s.add_dependency(%q<echoe>, [">= 0"])
|
35
|
+
end
|
36
|
+
else
|
37
|
+
s.add_dependency(%q<activerecord>, [">= 2.1.0"])
|
38
|
+
s.add_dependency(%q<builder>, [">= 0", "= 2.0"])
|
39
|
+
s.add_dependency(%q<echoe>, [">= 0"])
|
40
|
+
end
|
41
|
+
end
|
data/lib/invoicing.rb
ADDED
@@ -0,0 +1,9 @@
|
|
1
|
+
require 'activerecord'
|
2
|
+
|
3
|
+
require 'invoicing/class_info' # load first because other modules depend on this
|
4
|
+
Dir.glob(File.join(File.dirname(__FILE__), 'invoicing/**/*.rb')).sort.each {|f| require f }
|
5
|
+
|
6
|
+
# Mix all modules Invoicing::*::ActMethods into ActiveRecord::Base as class methods
|
7
|
+
Invoicing.constants.map{|c| Invoicing.const_get(c) }.select{|m| m.is_a?(Module) && m.const_defined?('ActMethods') }.each{
|
8
|
+
|m| ActiveRecord::Base.send(:extend, m.const_get('ActMethods'))
|
9
|
+
}
|
@@ -0,0 +1,107 @@
|
|
1
|
+
module Invoicing
|
2
|
+
# == Aggressive ActiveRecord cache
|
3
|
+
#
|
4
|
+
# This module implements a cache of +ActiveRecord+ objects. It is suitable for database
|
5
|
+
# tables with a small number of rows (no more than a few dozen is recommended) which
|
6
|
+
# change very infrequently. The contents of the table is loaded into memory when the
|
7
|
+
# class is first created; <b>to clear the cache you must call +clear_cache+ or
|
8
|
+
# restart the Ruby interpreter</b>. It is recommended that if you need to change the
|
9
|
+
# data in this table, you do so in a database migration, and apply that migration as
|
10
|
+
# part of a release deployment.
|
11
|
+
#
|
12
|
+
# The cache works as a simple identity map: it has a hash where the key is the primary
|
13
|
+
# key of each model object and the value is the model object itself. +ActiveRecord+
|
14
|
+
# methods are overridden so that if +find+ is called with one or more IDs, the object(s)
|
15
|
+
# are returned from cache; if +find+ is called with more complex conditions, the usual
|
16
|
+
# database mechanisms are used and the cache is ignored. Note that this does not
|
17
|
+
# guarantee that the same ID value will always map to the same model object instance;
|
18
|
+
# it just reduces the number of database queries.
|
19
|
+
#
|
20
|
+
# To activate +CachedRecord+, call +acts_as_cached_record+ in the scope of an
|
21
|
+
# <tt>ActiveRecord::Base</tt> class.
|
22
|
+
module CachedRecord
|
23
|
+
|
24
|
+
module ActMethods
|
25
|
+
# Call +acts_as_cached_record+ on an <tt>ActiveRecord::Base</tt> class to declare
|
26
|
+
# that objects of this class should be cached using +CachedRecord+.
|
27
|
+
#
|
28
|
+
# Accepts options in a hash, all of which are optional:
|
29
|
+
# * +id+ -- If the primary key of this model is not +id+, declare the method name
|
30
|
+
# of the primary key.
|
31
|
+
def acts_as_cached_record(*args)
|
32
|
+
Invoicing::ClassInfo.acts_as(Invoicing::CachedRecord, self, args)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
module ClassMethods
|
37
|
+
# This method overrides the default <tt>ActiveRecord::Base.find_from_ids</tt> (which is called
|
38
|
+
# from <tt>ActiveRecord::Base.find</tt>) with caching behaviour. +find+ is also used by
|
39
|
+
# +ActiveRecord+ when evaluating associations; therefore if another model object refers to
|
40
|
+
# a cached record by its ID, calling the getter of that association should result in a cache hit.
|
41
|
+
#
|
42
|
+
# FIXME: Currently +options+ is ignored -- we should do something more useful with it
|
43
|
+
# to ensure CachedRecord behaviour is fully compatible with +ActiveRecord+.
|
44
|
+
def find_from_ids(ids, options)
|
45
|
+
expects_array = ids.first.kind_of?(Array)
|
46
|
+
return ids.first if expects_array && ids.first.empty?
|
47
|
+
|
48
|
+
ids = ids.flatten.compact.uniq
|
49
|
+
|
50
|
+
case ids.size
|
51
|
+
when 0
|
52
|
+
raise ::ActiveRecord::RecordNotFound, "Couldn't find #{name} without an ID"
|
53
|
+
when 1
|
54
|
+
result = cached_record_class_info.find_one(ids.first, options)
|
55
|
+
expects_array ? [ result ] : result
|
56
|
+
else
|
57
|
+
cached_record_class_info.find_some(ids, options)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
# Returns a list of all objects of this class. Like <tt>ActiveRecord::Base.find(:all)</tt>
|
62
|
+
# but coming from the cache.
|
63
|
+
def cached_record_list
|
64
|
+
cached_record_class_info.list
|
65
|
+
end
|
66
|
+
|
67
|
+
# Reloads the cached objects from the database.
|
68
|
+
def reload_cache
|
69
|
+
cached_record_class_info.reload_cache
|
70
|
+
end
|
71
|
+
end # module ClassMethods
|
72
|
+
|
73
|
+
|
74
|
+
# Stores state in the ActiveRecord class object, including the cache --
|
75
|
+
# a hash which maps ID to model object for all objects of this model object type
|
76
|
+
class ClassInfo < Invoicing::ClassInfo::Base #:nodoc:
|
77
|
+
def initialize(model_class, previous_info, args)
|
78
|
+
super
|
79
|
+
reload_cache
|
80
|
+
end
|
81
|
+
|
82
|
+
def reload_cache
|
83
|
+
@cache = {}
|
84
|
+
model_class.find(:all).each {|obj| @cache[get(obj, :id)] = obj }
|
85
|
+
end
|
86
|
+
|
87
|
+
# Returns one object from the cache, given its ID.
|
88
|
+
def find_one(id, options)
|
89
|
+
if result = @cache[id]
|
90
|
+
result
|
91
|
+
else
|
92
|
+
raise ::ActiveRecord::RecordNotFound, "Couldn't find #{model_class.name} with ID=#{id}"
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
# Returns a list of objects from the cache, given a list of IDs.
|
97
|
+
def find_some(ids, options)
|
98
|
+
ids.map{|id| find_one(id, options) }
|
99
|
+
end
|
100
|
+
|
101
|
+
# Returns a list of all objects in the cache.
|
102
|
+
def list
|
103
|
+
@cache.values
|
104
|
+
end
|
105
|
+
end # class ClassInfo
|
106
|
+
end # module CachedRecord
|
107
|
+
end
|
@@ -0,0 +1,187 @@
|
|
1
|
+
module Invoicing
|
2
|
+
# This module is intended for use only internally within this framework. It implements
|
3
|
+
# a pattern needed in several other modules: an +acts_as_something_or_other+ method can
|
4
|
+
# be called within the scope of an +ActiveRecord+ class, given a number of arguments;
|
5
|
+
# including options which define how columns are renamed in a given model object.
|
6
|
+
# The information from these arguments needs to be stored in a class variable for later
|
7
|
+
# use in instances of that class. It must be possible to call the +acts_as_+ method
|
8
|
+
# multiple times, combining the arguments from the various calls, to make the whole thing
|
9
|
+
# look nicely declarative. Subclasses should inherit +acts_as_+ arguments from their
|
10
|
+
# superclass, but should be able to override them with their own values.
|
11
|
+
#
|
12
|
+
# This pattern assumes a particular module structure, like the following:
|
13
|
+
#
|
14
|
+
# module MyNamespace # you may use arbitrarily nested modules for namespacing (optional)
|
15
|
+
# module Teleporter # the name of this module defines auto-generated method names
|
16
|
+
# module ActMethods
|
17
|
+
# def acts_as_teleporter(*args) # should be called "acts_as_#{module_name.underscore}"
|
18
|
+
# Invoicing::ClassInfo.acts_as(MyNamespace::Teleporter, self, args)
|
19
|
+
# end
|
20
|
+
# end
|
21
|
+
#
|
22
|
+
# def transmogrify_the_instance # will become an instance method of the class on which the
|
23
|
+
# info = teleporter_class_info # acts_as_ method is called.
|
24
|
+
# info.do_transmogrify
|
25
|
+
# end
|
26
|
+
#
|
27
|
+
# module ClassMethods
|
28
|
+
# def transmogrify_the_class # will become a class method of the class on which the
|
29
|
+
# info = teleporter_class_info # acts_as_ method is called.
|
30
|
+
# info.do_transmogrify
|
31
|
+
# end
|
32
|
+
# end
|
33
|
+
#
|
34
|
+
# class ClassInfo < Invoicing::ClassInfo::Base
|
35
|
+
# def do_transmogrify
|
36
|
+
# case all_options[:transmogrification]
|
37
|
+
# when :total then "Transmogrified by #{all_args.first}"
|
38
|
+
# end
|
39
|
+
# end
|
40
|
+
# end
|
41
|
+
# end
|
42
|
+
# end
|
43
|
+
#
|
44
|
+
# ActiveRecord::Base.send(:extend, MyNamespace::Teleporter::ActMethods)
|
45
|
+
#
|
46
|
+
#
|
47
|
+
# +ClassInfo+ is used to store and process the arguments passed to the +acts_as_teleporter+ method
|
48
|
+
# when it is called in the scope of an +ActiveRecord+ model class. Finally, the feature defined by
|
49
|
+
# the +Teleporter+ module above can be used like this:
|
50
|
+
#
|
51
|
+
# class Teleporter < ActiveRecord::Base
|
52
|
+
# acts_as_teleporter 'Zoom2020', :transmogrification => :total
|
53
|
+
# end
|
54
|
+
#
|
55
|
+
# Teleporter.transmogrify_the_class # both return "Transmogrified by Zoom2020"
|
56
|
+
# Teleporter.find(42).transmogrify_the_instance
|
57
|
+
module ClassInfo
|
58
|
+
|
59
|
+
# Provides the main implementation pattern for an +acts_as_+ method. See the example above
|
60
|
+
# for usage.
|
61
|
+
# +source_module+:: The module object which is using the +ClassInfo+ pattern
|
62
|
+
# +calling_class+:: The class in whose scope the +acts_as_+ method was called
|
63
|
+
# +args+:: The array of arguments (including options hash) to the +acts_as_+ method
|
64
|
+
def self.acts_as(source_module, calling_class, args)
|
65
|
+
# The name by which the particular module using ClassInfo is known
|
66
|
+
module_name = source_module.name.split('::').last.underscore
|
67
|
+
class_info_method = "#{module_name}_class_info"
|
68
|
+
|
69
|
+
previous_info = if calling_class.private_instance_methods(true).include?(class_info_method)
|
70
|
+
# acts_as has been called before on the same class, or a superclass
|
71
|
+
calling_class.send(class_info_method)
|
72
|
+
else
|
73
|
+
# acts_as is being called for the first time -- do the mixins!
|
74
|
+
calling_class.send(:include, source_module)
|
75
|
+
calling_class.send(:extend, source_module.const_get('ClassMethods')) if source_module.constants.include? 'ClassMethods'
|
76
|
+
nil # no previous_info
|
77
|
+
end
|
78
|
+
|
79
|
+
# Instantiate the ClassInfo::Base subclass and assign it to an instance variable in calling_class
|
80
|
+
class_info_class = source_module.const_get('ClassInfo')
|
81
|
+
class_info = class_info_class.new(calling_class, previous_info, args)
|
82
|
+
calling_class.instance_variable_set("@#{class_info_method}", class_info)
|
83
|
+
|
84
|
+
# Define a getter class method on calling_class through which the ClassInfo::Base
|
85
|
+
# instance can be accessed.
|
86
|
+
calling_class.class_eval <<-CLASSEVAL
|
87
|
+
class << self
|
88
|
+
def #{class_info_method}
|
89
|
+
if superclass.private_instance_methods(true).include?("#{class_info_method}")
|
90
|
+
@#{class_info_method} ||= superclass.send("#{class_info_method}")
|
91
|
+
end
|
92
|
+
@#{class_info_method}
|
93
|
+
end
|
94
|
+
private "#{class_info_method}"
|
95
|
+
end
|
96
|
+
CLASSEVAL
|
97
|
+
|
98
|
+
# For convenience, also define an instance method which does the same as the class method
|
99
|
+
calling_class.class_eval do
|
100
|
+
define_method class_info_method do
|
101
|
+
self.class.send(class_info_method)
|
102
|
+
end
|
103
|
+
private class_info_method
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
|
108
|
+
# Base class for +ClassInfo+ objects, from which you need to derive a subclass in each module where
|
109
|
+
# you want to use +ClassInfo+. An instance of a <tt>ClassInfo::Base</tt> subclass is created every
|
110
|
+
# time an +acts_as_+ method is called, and that instance can be accessed through the
|
111
|
+
# +my_module_name_class_info+ method on the class which called +acts_as_my_module_name+.
|
112
|
+
class Base
|
113
|
+
# The class on which the +acts_as_+ method was called
|
114
|
+
attr_reader :model_class
|
115
|
+
|
116
|
+
# The <tt>ClassInfo::Base</tt> instance created by the last +acts_as_+ method
|
117
|
+
# call on the same class (or its superclass); +nil+ if this is the first call.
|
118
|
+
attr_reader :previous_info
|
119
|
+
|
120
|
+
# The list of arguments passed to the current +acts_as_+ method call (excluding the final options hash)
|
121
|
+
attr_reader :current_args
|
122
|
+
|
123
|
+
# Union of +current_args+ and <tt>previous_info.all_args</tt>
|
124
|
+
attr_reader :all_args
|
125
|
+
|
126
|
+
# <tt>self.all_args - previous_info.all_args</tt>
|
127
|
+
attr_reader :new_args
|
128
|
+
|
129
|
+
# The options hash passed to the current +acts_as_+ method call
|
130
|
+
attr_reader :current_options
|
131
|
+
|
132
|
+
# Hash of options with symbolized keys, with +option_defaults+ overridden by +previous_info+ options,
|
133
|
+
# in turn overridden by +current_options+.
|
134
|
+
attr_reader :all_options
|
135
|
+
|
136
|
+
# Initialises a <tt>ClassInfo::Base</tt> instance and parses arguments.
|
137
|
+
# If subclasses override +initialize+ they should call +super+.
|
138
|
+
# +model_class+:: The class on which the +acts_as+ method was called
|
139
|
+
# +previous_info+:: The <tt>ClassInfo::Base</tt> instance created by the last +acts_as_+ method
|
140
|
+
# call on the same class (or its superclass); +nil+ if this is the first call.
|
141
|
+
# +args+:: Array of arguments given to the +acts_as_+ method when it was invoked.
|
142
|
+
#
|
143
|
+
# If the last element of +args+ is a hash, it is used as an options array. All other elements
|
144
|
+
# of +args+ are concatenated into an array, +uniq+ed and flattened. (They could be a list of symbols
|
145
|
+
# representing method names, for example.)
|
146
|
+
def initialize(model_class, previous_info, args)
|
147
|
+
@model_class = model_class
|
148
|
+
@previous_info = previous_info
|
149
|
+
|
150
|
+
@current_options = args.extract_options!.symbolize_keys
|
151
|
+
@all_options = (@previous_info.nil? ? option_defaults : @previous_info.all_options).clone
|
152
|
+
@all_options.update(@current_options)
|
153
|
+
|
154
|
+
@all_args = @new_args = @current_args = args.flatten.uniq
|
155
|
+
unless @previous_info.nil?
|
156
|
+
@all_args = (@previous_info.all_args + @all_args).uniq
|
157
|
+
@new_args = @all_args - previous_info.all_args
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
161
|
+
# Override this method to return a hash of default option values.
|
162
|
+
def option_defaults
|
163
|
+
{}
|
164
|
+
end
|
165
|
+
|
166
|
+
# If there is an option with the given key, returns the associated value; otherwise returns
|
167
|
+
# the key. This is useful for mapping method names to their renamed equivalents through options.
|
168
|
+
def method(name)
|
169
|
+
name = name.to_sym
|
170
|
+
(all_options[name] || name).to_s
|
171
|
+
end
|
172
|
+
|
173
|
+
# Returns the value returned by calling +method_name+ (renamed through options using +method+)
|
174
|
+
# on +object+. Returns +nil+ if +object+ is +nil+ or +object+ does not respond to that method.
|
175
|
+
def get(object, method_name)
|
176
|
+
meth = method(method_name)
|
177
|
+
(object.nil? || !object.respond_to?(meth)) ? nil : object.send(meth)
|
178
|
+
end
|
179
|
+
|
180
|
+
# Assigns +new_value+ to <tt>method_name=</tt> (renamed through options using +method+)
|
181
|
+
# on +object+. +method_name+ should not include the equals sign.
|
182
|
+
def set(object, method_name, new_value)
|
183
|
+
object.send("#{method(method_name)}=", new_value) unless object.nil?
|
184
|
+
end
|
185
|
+
end
|
186
|
+
end
|
187
|
+
end
|