mochigome 0.0.3 → 0.0.4
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/Gemfile +2 -0
- data/Gemfile.lock +4 -0
- data/Rakefile +2 -1
- data/lib/arel_rails2_hacks.rb +49 -0
- data/lib/data_node.rb +4 -0
- data/lib/exceptions.rb +1 -0
- data/lib/mochigome.rb +1 -0
- data/lib/mochigome_ver.rb +1 -1
- data/lib/model_extensions.rb +202 -122
- data/lib/query.rb +295 -148
- data/test/app_root/app/models/category.rb +4 -2
- data/test/app_root/app/models/product.rb +14 -5
- data/test/app_root/app/models/sale.rb +3 -1
- data/test/app_root/config/initializers/arel.rb +2 -0
- data/test/app_root/db/migrate/20110817163830_create_tables.rb +0 -1
- data/test/console.sh +6 -0
- data/test/factories.rb +1 -2
- data/test/test_helper.rb +49 -49
- data/test/unit/data_node_test.rb +7 -0
- data/test/unit/model_extensions_test.rb +110 -93
- data/test/unit/query_test.rb +233 -64
- metadata +32 -14
data/Gemfile
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
source :rubygems
|
2
2
|
|
3
3
|
gem 'rails', '2.3.12'
|
4
|
+
gem 'arel', '~> 2.1'
|
4
5
|
gem 'nokogiri'
|
5
6
|
gem 'ruport'
|
6
7
|
gem 'rgl'
|
@@ -8,6 +9,7 @@ gem 'rgl'
|
|
8
9
|
gem 'sqlite3'
|
9
10
|
gem 'mysql'
|
10
11
|
gem 'factory_girl', '2.0.4'
|
12
|
+
gem 'rdoc'
|
11
13
|
gem 'rcov'
|
12
14
|
gem 'ruby-prof'
|
13
15
|
gem 'minitest'
|
data/Gemfile.lock
CHANGED
@@ -11,6 +11,7 @@ GEM
|
|
11
11
|
activeresource (2.3.12)
|
12
12
|
activesupport (= 2.3.12)
|
13
13
|
activesupport (2.3.12)
|
14
|
+
arel (2.1.4)
|
14
15
|
autowatchr (0.1.5)
|
15
16
|
watchr
|
16
17
|
color (1.4.1)
|
@@ -37,6 +38,7 @@ GEM
|
|
37
38
|
rake (>= 0.8.3)
|
38
39
|
rake (0.9.2)
|
39
40
|
rcov (0.9.10)
|
41
|
+
rdoc (3.9.4)
|
40
42
|
rev (0.3.2)
|
41
43
|
iobuffer (>= 0.1.3)
|
42
44
|
rgl (0.4.0)
|
@@ -57,6 +59,7 @@ PLATFORMS
|
|
57
59
|
ruby
|
58
60
|
|
59
61
|
DEPENDENCIES
|
62
|
+
arel (~> 2.1)
|
60
63
|
autowatchr
|
61
64
|
factory_girl (= 2.0.4)
|
62
65
|
minitest
|
@@ -65,6 +68,7 @@ DEPENDENCIES
|
|
65
68
|
nokogiri
|
66
69
|
rails (= 2.3.12)
|
67
70
|
rcov
|
71
|
+
rdoc
|
68
72
|
rev
|
69
73
|
rgl
|
70
74
|
ruby-prof
|
data/Rakefile
CHANGED
@@ -59,12 +59,13 @@ gemspec = Gem::Specification.new do |s|
|
|
59
59
|
s.email = "david.mike.simon@gmail.com"
|
60
60
|
s.homepage = "http://github.com/DavidMikeSimon/mochigome"
|
61
61
|
s.summary = "User-customizable report generator"
|
62
|
-
s.description = "
|
62
|
+
s.description = "Report generator that uses your ActiveRecord associations"
|
63
63
|
s.files = `git ls-files .`.split("\n") - [".gitignore"]
|
64
64
|
s.platform = Gem::Platform::RUBY
|
65
65
|
s.require_path = 'lib'
|
66
66
|
s.rubyforge_project = '[none]'
|
67
67
|
|
68
|
+
s.add_dependency('arel', '~> 2.1')
|
68
69
|
s.add_dependency('ruport')
|
69
70
|
s.add_dependency('nokogiri')
|
70
71
|
s.add_dependency('rgl')
|
@@ -0,0 +1,49 @@
|
|
1
|
+
unless ActiveRecord::ConnectionAdapters::ConnectionPool.methods.include?("table_exists?")
|
2
|
+
module Mochigome
|
3
|
+
class ColumnsHashProxy
|
4
|
+
def initialize(pool)
|
5
|
+
@pool = pool
|
6
|
+
@cache = HashWithIndifferentAccess.new
|
7
|
+
end
|
8
|
+
|
9
|
+
def [](table_name)
|
10
|
+
cached(table_name)
|
11
|
+
end
|
12
|
+
|
13
|
+
private
|
14
|
+
|
15
|
+
def cached(table_name)
|
16
|
+
return @cache[table_name] if @cache.has_key?(:table_name)
|
17
|
+
|
18
|
+
@cache[table_name] = h = HashWithIndifferentAccess.new
|
19
|
+
@pool.with_connection do |c|
|
20
|
+
c.columns(table_name).each do |col|
|
21
|
+
h[col.name] = col
|
22
|
+
end
|
23
|
+
end
|
24
|
+
return h
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
class ActiveRecord::ConnectionAdapters::ConnectionPool
|
30
|
+
def table_exists?(name)
|
31
|
+
ActiveRecord::Base.connection_pool.with_connection do |c|
|
32
|
+
c.table_exists?(name)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
@@columns_hash_proxy = nil
|
37
|
+
def columns_hash
|
38
|
+
@@columns_hash_proxy ||= Mochigome::ColumnsHashProxy.new(self)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
class ActiveRecord::ConnectionAdapters::SQLiteAdapter
|
43
|
+
def select_rows(sql, name = nil)
|
44
|
+
execute(sql, name).map do |row|
|
45
|
+
row.keys.select{|key| key.is_a? Integer}.map{|key| row[key]}
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
data/lib/data_node.rb
CHANGED
data/lib/exceptions.rb
CHANGED
data/lib/mochigome.rb
CHANGED
data/lib/mochigome_ver.rb
CHANGED
data/lib/model_extensions.rb
CHANGED
@@ -1,9 +1,4 @@
|
|
1
1
|
module Mochigome
|
2
|
-
@reportFocusModels = []
|
3
|
-
def self.reportFocusModels
|
4
|
-
@reportFocusModels
|
5
|
-
end
|
6
|
-
|
7
2
|
module ModelExtensions
|
8
3
|
def self.included(base)
|
9
4
|
base.extend(ClassMethods)
|
@@ -11,75 +6,68 @@ module Mochigome
|
|
11
6
|
base.write_inheritable_attribute :mochigome_focus_settings, nil
|
12
7
|
base.class_inheritable_reader :mochigome_focus_settings
|
13
8
|
|
14
|
-
|
15
|
-
base.
|
9
|
+
# TODO: Use an ordered hash for this
|
10
|
+
base.write_inheritable_attribute :mochigome_aggregation_settings, nil
|
11
|
+
base.class_inheritable_reader :mochigome_aggregation_settings
|
16
12
|
end
|
17
13
|
|
18
14
|
module ClassMethods
|
19
15
|
def acts_as_mochigome_focus
|
20
|
-
if self.try(:mochigome_focus_settings).try(:
|
16
|
+
if self.try(:mochigome_focus_settings).try(:model) == self
|
21
17
|
raise Mochigome::ModelSetupError.new("Already acts_as_mochigome_focus for #{self.name}")
|
22
18
|
end
|
23
19
|
settings = ReportFocusSettings.new(self)
|
24
20
|
yield settings if block_given?
|
25
21
|
write_inheritable_attribute :mochigome_focus_settings, settings
|
26
22
|
send(:include, InstanceMethods)
|
27
|
-
Mochigome::reportFocusModels << self
|
28
23
|
end
|
29
24
|
|
30
25
|
def acts_as_mochigome_focus?
|
31
26
|
!!mochigome_focus_settings
|
32
27
|
end
|
33
28
|
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
'minimum' => 'min(%s)',
|
40
|
-
'min' => 'min(%s)',
|
41
|
-
'maximum' => 'max(%s)',
|
42
|
-
'max' => 'max(%s)',
|
43
|
-
'sum' => 'sum(%s)'
|
44
|
-
}
|
45
|
-
|
46
|
-
def has_mochigome_aggregations(aggregations)
|
47
|
-
unless aggregations.respond_to?(:each)
|
48
|
-
raise ModelSetupError.new "Call has_mochigome_aggregations with an Enumerable"
|
29
|
+
# TODO: Split out aggregation stuff into its own module
|
30
|
+
|
31
|
+
def has_mochigome_aggregations
|
32
|
+
if self.try(:mochigome_aggregation_settings).try(:model) == self
|
33
|
+
raise Mochigome::ModelSetupError.new("Already aggregation settings for #{self.name}")
|
49
34
|
end
|
35
|
+
settings = AggregationSettings.new(self)
|
36
|
+
yield settings if block_given?
|
37
|
+
write_inheritable_attribute :mochigome_aggregation_settings, settings
|
38
|
+
end
|
50
39
|
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
40
|
+
def arelified_assoc(name)
|
41
|
+
# TODO: Deal with polymorphic assocs.
|
42
|
+
model = self
|
43
|
+
assoc = reflect_on_association(name)
|
44
|
+
raise AssociationError.new("No such assoc #{name}") unless assoc
|
45
|
+
table = Arel::Table.new(table_name)
|
46
|
+
ftable = Arel::Table.new(assoc.klass.table_name)
|
47
|
+
lambda do |r|
|
48
|
+
# FIXME: This acts as though arel methods are non-destructive,
|
49
|
+
# but they are, right? Except, I can't remove the rel
|
50
|
+
# assignment from relation_over_path...
|
51
|
+
cond = nil
|
52
|
+
if assoc.belongs_to?
|
53
|
+
cond = table[assoc.association_foreign_key].eq(
|
54
|
+
ftable[assoc.klass.primary_key]
|
55
|
+
)
|
56
|
+
else
|
57
|
+
cond = table[primary_key].eq(ftable[assoc.primary_key_name])
|
68
58
|
end
|
69
|
-
raise ModelSetupError.new "Invalid aggregation expr: #{obj.inspect}"
|
70
|
-
end
|
71
59
|
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
{:name => "%s %s" % [name.pluralize, f.to_s.sub("_", " ")]}.merge(aggregation_opts(f))
|
76
|
-
when Hash then
|
77
|
-
{:name => f.keys.first.to_s}.merge(aggregation_opts(f.values.first))
|
78
|
-
else raise ModelSetupError.new "Invalid aggregation: #{f.inspect}"
|
60
|
+
if assoc.options[:as]
|
61
|
+
# FIXME Can we assume that this is the polymorphic type field?
|
62
|
+
cond = cond.and(ftable["#{assoc.options[:as]}_type"].eq(model.name))
|
79
63
|
end
|
64
|
+
|
65
|
+
# TODO: Apply association conditions.
|
66
|
+
|
67
|
+
r.join(ftable, Arel::Nodes::OuterJoin).on(cond)
|
80
68
|
end
|
81
|
-
mochigome_aggregations.concat(additions)
|
82
69
|
end
|
70
|
+
|
83
71
|
end
|
84
72
|
|
85
73
|
module InstanceMethods
|
@@ -89,6 +77,20 @@ module Mochigome
|
|
89
77
|
end
|
90
78
|
end
|
91
79
|
|
80
|
+
# FIXME This probably doesn't belong here. Maybe I should have a module
|
81
|
+
# for this kind of stuff and also put the standard aggregation functions
|
82
|
+
# in there?
|
83
|
+
|
84
|
+
def self.null_unless(pred, value_func)
|
85
|
+
lambda {|t|
|
86
|
+
value = value_func.call(t)
|
87
|
+
val_expr = Arel::Nodes::NamedFunction.new('',[value])
|
88
|
+
Arel::Nodes::SqlLiteral.new(
|
89
|
+
"(CASE WHEN #{pred.call(value).to_sql} THEN #{val_expr.to_sql} ELSE NULL END)"
|
90
|
+
)
|
91
|
+
}
|
92
|
+
end
|
93
|
+
|
92
94
|
private
|
93
95
|
|
94
96
|
class ReportFocus
|
@@ -98,7 +100,7 @@ module Mochigome
|
|
98
100
|
def initialize(owner, settings)
|
99
101
|
@owner = owner
|
100
102
|
@name_proc = settings.options[:name] || lambda{|obj| obj.name}
|
101
|
-
@type_name = settings.options[:type_name] || owner.class.
|
103
|
+
@type_name = settings.options[:type_name] || owner.class.human_name
|
102
104
|
@fields = settings.options[:fields] || []
|
103
105
|
end
|
104
106
|
|
@@ -106,10 +108,6 @@ module Mochigome
|
|
106
108
|
@name_proc.call(@owner)
|
107
109
|
end
|
108
110
|
|
109
|
-
def data(options = {})
|
110
|
-
field_data.merge(aggregate_data(:all, options))
|
111
|
-
end
|
112
|
-
|
113
111
|
def field_data
|
114
112
|
h = ActiveSupport::OrderedHash.new
|
115
113
|
self.fields.each do |field|
|
@@ -117,66 +115,15 @@ module Mochigome
|
|
117
115
|
end
|
118
116
|
h
|
119
117
|
end
|
120
|
-
|
121
|
-
def aggregate_data(assoc_name, options = {})
|
122
|
-
h = ActiveSupport::OrderedHash.new
|
123
|
-
assoc_name = assoc_name.to_sym
|
124
|
-
if assoc_name == :all
|
125
|
-
@owner.class.reflections.each do |name, assoc|
|
126
|
-
h.merge! aggregate_data(name, options)
|
127
|
-
end
|
128
|
-
else
|
129
|
-
assoc = @owner.class.reflections[assoc_name]
|
130
|
-
assoc_object = @owner
|
131
|
-
# TODO: Are there other ways context could matter besides :through assocs?
|
132
|
-
# TODO: What if a through reflection goes through _another_ through reflection?
|
133
|
-
if options.has_key?(:context) && assoc.through_reflection
|
134
|
-
# FIXME: This seems like it's repeating Query work
|
135
|
-
join_objs = assoc_object.send(assoc.through_reflection.name)
|
136
|
-
options[:context].each do |obj|
|
137
|
-
next unless join_objs.include?(obj)
|
138
|
-
assoc = assoc.source_reflection
|
139
|
-
assoc_object = obj
|
140
|
-
break
|
141
|
-
end
|
142
|
-
end
|
143
|
-
assoc.klass.mochigome_aggregations.each do |agg|
|
144
|
-
# TODO: There *must* be a better way to do this query
|
145
|
-
# It's ugly, involves an ActiveRecord creation, and causes lots of DB hits
|
146
|
-
sel_expr = "(#{agg[:expr]}) AS x"
|
147
|
-
cond_expr = agg[:conditions] ? agg[:conditions] : "1=1"
|
148
|
-
if assoc.belongs_to? # FIXME: or has_one
|
149
|
-
obj = assoc_object.send(assoc.name)
|
150
|
-
row = obj.class.find(obj.id, :select => sel_expr, :conditions => cond_expr)
|
151
|
-
else
|
152
|
-
row = assoc_object.send(assoc.name).first(:select => sel_expr, :conditions => cond_expr)
|
153
|
-
end
|
154
|
-
h[agg[:name]] = self.class.auto_numerify(row.x)
|
155
|
-
end
|
156
|
-
end
|
157
|
-
h
|
158
|
-
end
|
159
|
-
|
160
|
-
def self.auto_numerify(data)
|
161
|
-
# It's already some more specific type, leave it alone
|
162
|
-
return data unless data.is_a?(String)
|
163
|
-
|
164
|
-
# Try to turn data into an integer or float if appropriate
|
165
|
-
data = data.strip
|
166
|
-
if data =~ /\A[+-]?\d+(\.\d+)?\Z/
|
167
|
-
return ($1 and !$1.blank?) ? data.to_f : data.to_i
|
168
|
-
else
|
169
|
-
return data
|
170
|
-
end
|
171
|
-
end
|
172
118
|
end
|
173
119
|
|
174
120
|
class ReportFocusSettings
|
175
121
|
attr_reader :options
|
176
|
-
attr_reader :
|
122
|
+
attr_reader :model
|
123
|
+
attr_reader :ordering
|
177
124
|
|
178
|
-
def initialize(
|
179
|
-
@
|
125
|
+
def initialize(model)
|
126
|
+
@model = model
|
180
127
|
@options = {}
|
181
128
|
@options[:fields] = []
|
182
129
|
end
|
@@ -192,16 +139,15 @@ module Mochigome
|
|
192
139
|
@options[:name] = n.to_proc
|
193
140
|
end
|
194
141
|
|
195
|
-
def
|
196
|
-
|
197
|
-
|
198
|
-
if s.gsub(/ +/, "_").underscore == reserved
|
199
|
-
raise ModelSetupError.new "Field name \"#{s}\" conflicts with reserved term \"#{reserved}\""
|
200
|
-
end
|
201
|
-
end
|
202
|
-
s
|
203
|
-
end
|
142
|
+
def ordering(n)
|
143
|
+
@options[:ordering] = n.to_s
|
144
|
+
end
|
204
145
|
|
146
|
+
def get_ordering
|
147
|
+
@options[:ordering] || @model.primary_key.to_s
|
148
|
+
end
|
149
|
+
|
150
|
+
def fields(fields)
|
205
151
|
unless fields.respond_to?(:each)
|
206
152
|
raise ModelSetupError.new "Call f.fields with an Enumerable"
|
207
153
|
end
|
@@ -209,11 +155,11 @@ module Mochigome
|
|
209
155
|
@options[:fields] += fields.map do |f|
|
210
156
|
case f
|
211
157
|
when String, Symbol then {
|
212
|
-
:name =>
|
158
|
+
:name => Mochigome::complain_if_reserved_name(f.to_s.strip),
|
213
159
|
:value_func => lambda{|obj| obj.send(f.to_sym)}
|
214
160
|
}
|
215
161
|
when Hash then {
|
216
|
-
:name =>
|
162
|
+
:name => Mochigome::complain_if_reserved_name(f.keys.first.to_s.strip),
|
217
163
|
:value_func => f.values.first.to_proc
|
218
164
|
}
|
219
165
|
else raise ModelSetupError.new "Invalid field: #{f.inspect}"
|
@@ -221,4 +167,138 @@ module Mochigome
|
|
221
167
|
end
|
222
168
|
end
|
223
169
|
end
|
170
|
+
|
171
|
+
class AggregationSettings
|
172
|
+
attr_reader :options
|
173
|
+
attr_reader :model
|
174
|
+
|
175
|
+
def initialize(model)
|
176
|
+
@model = model
|
177
|
+
@options = {}
|
178
|
+
@options[:fields] = []
|
179
|
+
end
|
180
|
+
|
181
|
+
def fields(aggs)
|
182
|
+
unless aggs.is_a?(Enumerable)
|
183
|
+
raise ModelSetupError.new "Call a.fields with an Enumerable"
|
184
|
+
end
|
185
|
+
|
186
|
+
@options[:fields].concat(aggs.map {|f|
|
187
|
+
case f
|
188
|
+
when String, Symbol then
|
189
|
+
{
|
190
|
+
:name => "%s %s" % [@model.name.pluralize, f.to_s.sub("_", " ")]
|
191
|
+
}.merge(Mochigome::split_out_aggregation_procs(f))
|
192
|
+
when Hash then
|
193
|
+
if f.size == 1
|
194
|
+
{
|
195
|
+
:name => f.keys.first.to_s
|
196
|
+
}.merge(Mochigome::split_out_aggregation_procs(f.values.first))
|
197
|
+
else
|
198
|
+
{
|
199
|
+
:name => f[:name],
|
200
|
+
:value_proc => Mochigome::value_proc(f[:value]),
|
201
|
+
:agg_proc => Mochigome::aggregation_proc(f[:aggregation])
|
202
|
+
}
|
203
|
+
end
|
204
|
+
else
|
205
|
+
raise ModelSetupError.new "Invalid aggregation: #{f.inspect}"
|
206
|
+
end
|
207
|
+
})
|
208
|
+
end
|
209
|
+
|
210
|
+
def hidden_fields(aggs)
|
211
|
+
orig_keys = Set.new @options[:fields].map{|a| a[:name]}
|
212
|
+
fields(aggs)
|
213
|
+
@options[:fields].each do |h|
|
214
|
+
next if orig_keys.include? h[:name]
|
215
|
+
h[:hidden] = true
|
216
|
+
end
|
217
|
+
end
|
218
|
+
|
219
|
+
def fields_in_ruby(aggs)
|
220
|
+
@options[:fields].concat(aggs.map {|f|
|
221
|
+
raise ModelSetupError.new "Invalid ruby agg #{f.inspect}" unless f.is_a?(Hash)
|
222
|
+
{
|
223
|
+
:name => f.keys.first.to_s,
|
224
|
+
:ruby_proc => f.values.first,
|
225
|
+
:in_ruby => true
|
226
|
+
}
|
227
|
+
})
|
228
|
+
end
|
229
|
+
end
|
230
|
+
|
231
|
+
def self.complain_if_reserved_name(s)
|
232
|
+
test_s = s.gsub(/ +/, "_").underscore
|
233
|
+
['name', 'id', 'type', 'internal_type'].each do |reserved|
|
234
|
+
if test_s == reserved
|
235
|
+
raise ModelSetupError.new "Field name \"#{s}\" conflicts with reserved term \"#{reserved}\""
|
236
|
+
end
|
237
|
+
end
|
238
|
+
s
|
239
|
+
end
|
240
|
+
|
241
|
+
AGGREGATION_FUNCS = {
|
242
|
+
:count => lambda{|a| a.count},
|
243
|
+
:distinct => lambda{|a| a.count(true)},
|
244
|
+
:average => lambda{|a| a.average},
|
245
|
+
:avg => :average,
|
246
|
+
:minimum => lambda{|a| a.minimum},
|
247
|
+
:min => :minimum,
|
248
|
+
:maximum => lambda{|a| a.maximum},
|
249
|
+
:max => :maximum,
|
250
|
+
:sum => lambda{|a| a.sum}
|
251
|
+
}
|
252
|
+
|
253
|
+
# Given an object, tries to coerce it into a proc that takes a node
|
254
|
+
# and returns an expression node to collect some aggregate data from it.
|
255
|
+
def self.aggregation_proc(obj)
|
256
|
+
if obj.is_a?(Symbol)
|
257
|
+
orig_name = obj
|
258
|
+
2.times do
|
259
|
+
# Lookup twice to allow for indirect aliases in AGGREGATION_FUNCS
|
260
|
+
obj = AGGREGATION_FUNCS[obj] if obj.is_a?(Symbol)
|
261
|
+
end
|
262
|
+
raise ModelSetupError.new "Can't find aggregation function #{orig_name}" unless obj
|
263
|
+
obj
|
264
|
+
elsif obj.is_a?(Proc)
|
265
|
+
obj
|
266
|
+
else
|
267
|
+
raise ModelSetupError.new "Invalid aggregation function #{obj.inspect}"
|
268
|
+
end
|
269
|
+
end
|
270
|
+
|
271
|
+
# Given an object, tries to coerce it into a proc that takes a relation
|
272
|
+
# and returns a node for some value in it to be aggregated over
|
273
|
+
def self.value_proc(obj)
|
274
|
+
if obj.is_a?(Symbol)
|
275
|
+
lambda {|t| t[obj]}
|
276
|
+
elsif obj.is_a?(Proc)
|
277
|
+
obj
|
278
|
+
else
|
279
|
+
raise ModelSetupError.new "Invalid value function #{obj.inspect}"
|
280
|
+
end
|
281
|
+
end
|
282
|
+
|
283
|
+
def self.split_out_aggregation_procs(obj)
|
284
|
+
case obj
|
285
|
+
when Symbol, String
|
286
|
+
vals = obj.to_s.split(/[ _]/).map(&:downcase).map(&:to_sym)
|
287
|
+
when Array
|
288
|
+
vals = obj.dup
|
289
|
+
else
|
290
|
+
raise ModelSetupError.new "Invalid aggregation type: #{obj.inspect}"
|
291
|
+
end
|
292
|
+
|
293
|
+
if vals.size == 1
|
294
|
+
vals << :id # TODO : Use real primary key
|
295
|
+
elsif vals.size != 2
|
296
|
+
raise ModelSetupError.new "Wrong # of components for agg: #{obj.inspect}"
|
297
|
+
end
|
298
|
+
|
299
|
+
{
|
300
|
+
:agg_proc => aggregation_proc(vals.first),
|
301
|
+
:value_proc => value_proc(vals.last)
|
302
|
+
}
|
303
|
+
end
|
224
304
|
end
|