invoicing 0.1.0
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/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
|