mochigome 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- data/.autotest +14 -0
- data/Gemfile +16 -0
- data/Gemfile.lock +71 -0
- data/LICENSE +674 -0
- data/README.rdoc +11 -0
- data/Rakefile +74 -0
- data/TODO +6 -0
- data/lib/data_node.rb +106 -0
- data/lib/exceptions.rb +6 -0
- data/lib/mochigome.rb +7 -0
- data/lib/mochigome_ver.rb +3 -0
- data/lib/model_extensions.rb +211 -0
- data/lib/query.rb +199 -0
- data/test/app_root/app/controllers/application_controller.rb +2 -0
- data/test/app_root/app/controllers/owners_controller.rb +2 -0
- data/test/app_root/app/models/boring_datum.rb +3 -0
- data/test/app_root/app/models/category.rb +7 -0
- data/test/app_root/app/models/owner.rb +17 -0
- data/test/app_root/app/models/product.rb +21 -0
- data/test/app_root/app/models/sale.rb +9 -0
- data/test/app_root/app/models/store.rb +13 -0
- data/test/app_root/app/models/store_product.rb +11 -0
- data/test/app_root/config/boot.rb +130 -0
- data/test/app_root/config/database-pg.yml +8 -0
- data/test/app_root/config/database.yml +6 -0
- data/test/app_root/config/environment.rb +14 -0
- data/test/app_root/config/environments/test.rb +20 -0
- data/test/app_root/config/offroad.yml +6 -0
- data/test/app_root/config/preinitializer.rb +20 -0
- data/test/app_root/config/routes.rb +4 -0
- data/test/app_root/db/migrate/20110817163830_create_tables.rb +66 -0
- data/test/app_root/vendor/plugins/mochigome/init.rb +2 -0
- data/test/factories.rb +39 -0
- data/test/test.watchr +6 -0
- data/test/test_helper.rb +66 -0
- data/test/unit/data_node_test.rb +144 -0
- data/test/unit/model_extensions_test.rb +367 -0
- data/test/unit/query_test.rb +202 -0
- metadata +143 -0
@@ -0,0 +1,367 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/../test_helper')
|
2
|
+
|
3
|
+
describe "an ActiveRecord model" do
|
4
|
+
before do
|
5
|
+
@model_class = Class.new(ActiveRecord::Base)
|
6
|
+
@model_class.class_eval do
|
7
|
+
set_table_name :fake
|
8
|
+
def name
|
9
|
+
"Moby"
|
10
|
+
end
|
11
|
+
def last_name
|
12
|
+
"Dick"
|
13
|
+
end
|
14
|
+
end
|
15
|
+
Whale = @model_class
|
16
|
+
end
|
17
|
+
|
18
|
+
after do
|
19
|
+
Object.send(:remove_const, :Whale)
|
20
|
+
end
|
21
|
+
|
22
|
+
it "indicates if it acts_as_mochigome_focus or not" do
|
23
|
+
refute @model_class.acts_as_mochigome_focus?
|
24
|
+
@model_class.class_eval do
|
25
|
+
acts_as_mochigome_focus
|
26
|
+
end
|
27
|
+
assert @model_class.acts_as_mochigome_focus?
|
28
|
+
end
|
29
|
+
|
30
|
+
it "cannot call acts_as_mochigome_focus more than once" do
|
31
|
+
@model_class.class_eval do
|
32
|
+
acts_as_mochigome_focus
|
33
|
+
end
|
34
|
+
assert_raises Mochigome::ModelSetupError do
|
35
|
+
@model_class.class_eval do
|
36
|
+
acts_as_mochigome_focus
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
it "inherits a parent's report focus settings" do
|
42
|
+
@model_class.class_eval do
|
43
|
+
acts_as_mochigome_focus do |f|
|
44
|
+
f.type_name "Foobar"
|
45
|
+
end
|
46
|
+
end
|
47
|
+
@sub_class = Class.new(@model_class)
|
48
|
+
i = @sub_class.new
|
49
|
+
assert_equal "Foobar", i.mochigome_focus.type_name
|
50
|
+
end
|
51
|
+
|
52
|
+
it "can override a parent's report focus settings" do
|
53
|
+
@model_class.class_eval do
|
54
|
+
acts_as_mochigome_focus do |f|
|
55
|
+
f.type_name "Foobar"
|
56
|
+
end
|
57
|
+
end
|
58
|
+
@sub_class = Class.new(@model_class)
|
59
|
+
@sub_class.class_eval do
|
60
|
+
acts_as_mochigome_focus do |f|
|
61
|
+
f.type_name "Narfbork"
|
62
|
+
end
|
63
|
+
end
|
64
|
+
i = @sub_class.new
|
65
|
+
assert_equal "Narfbork", i.mochigome_focus.type_name
|
66
|
+
end
|
67
|
+
|
68
|
+
it "uses its class name as the default type name" do
|
69
|
+
@model_class.class_eval do
|
70
|
+
acts_as_mochigome_focus
|
71
|
+
end
|
72
|
+
i = @model_class.new
|
73
|
+
assert_equal "Whale", i.mochigome_focus.type_name.split("::").last
|
74
|
+
end
|
75
|
+
|
76
|
+
it "can override the default type name" do
|
77
|
+
@model_class.class_eval do
|
78
|
+
acts_as_mochigome_focus do |f|
|
79
|
+
f.type_name "Thingie"
|
80
|
+
end
|
81
|
+
end
|
82
|
+
i = @model_class.new
|
83
|
+
assert_equal "Thingie", i.mochigome_focus.type_name
|
84
|
+
end
|
85
|
+
|
86
|
+
it "cannot specify a nonsense type name" do
|
87
|
+
assert_raises Mochigome::ModelSetupError do
|
88
|
+
@model_class.class_eval do
|
89
|
+
acts_as_mochigome_focus do |f|
|
90
|
+
f.type_name 12345
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
it "uses the attribute 'name' as the default focus name" do
|
97
|
+
@model_class.class_eval do
|
98
|
+
acts_as_mochigome_focus
|
99
|
+
end
|
100
|
+
i = @model_class.new
|
101
|
+
assert_equal "Moby", i.mochigome_focus.name
|
102
|
+
end
|
103
|
+
|
104
|
+
it "can override the focus name with another method_name" do
|
105
|
+
@model_class.class_eval do
|
106
|
+
acts_as_mochigome_focus do |f|
|
107
|
+
f.name :last_name
|
108
|
+
end
|
109
|
+
end
|
110
|
+
i = @model_class.new
|
111
|
+
assert_equal "Dick", i.mochigome_focus.name
|
112
|
+
end
|
113
|
+
|
114
|
+
it "can override the focus name with a custom implementation" do
|
115
|
+
@model_class.class_eval do
|
116
|
+
acts_as_mochigome_focus do |f|
|
117
|
+
f.name lambda {|obj| "#{obj.name} #{obj.last_name}"}
|
118
|
+
end
|
119
|
+
end
|
120
|
+
i = @model_class.new
|
121
|
+
assert_equal "Moby Dick", i.mochigome_focus.name
|
122
|
+
end
|
123
|
+
|
124
|
+
it "can specify fields" do
|
125
|
+
@model_class.class_eval do
|
126
|
+
acts_as_mochigome_focus do |f|
|
127
|
+
f.fields ["a", "b"]
|
128
|
+
end
|
129
|
+
end
|
130
|
+
i = @model_class.new(:a => "abc", :b => "xyz")
|
131
|
+
expected = ActiveSupport::OrderedHash.new
|
132
|
+
expected["a"] = "abc"
|
133
|
+
expected["b"] = "xyz"
|
134
|
+
assert_equal expected, i.mochigome_focus.field_data
|
135
|
+
end
|
136
|
+
|
137
|
+
it "has no report focus data if no fields are specified" do
|
138
|
+
@model_class.class_eval do
|
139
|
+
acts_as_mochigome_focus
|
140
|
+
end
|
141
|
+
i = @model_class.new(:a => "abc", :b => "xyz")
|
142
|
+
assert_empty i.mochigome_focus.field_data
|
143
|
+
end
|
144
|
+
|
145
|
+
it "can specify only some of its fields" do
|
146
|
+
@model_class.class_eval do
|
147
|
+
acts_as_mochigome_focus do |f|
|
148
|
+
f.fields ["b"]
|
149
|
+
end
|
150
|
+
end
|
151
|
+
i = @model_class.new(:a => "abc", :b => "xyz")
|
152
|
+
expected = ActiveSupport::OrderedHash.new
|
153
|
+
expected["b"] = "xyz"
|
154
|
+
assert_equal expected, i.mochigome_focus.field_data
|
155
|
+
end
|
156
|
+
|
157
|
+
it "can specify fields in a custom order" do
|
158
|
+
@model_class.class_eval do
|
159
|
+
acts_as_mochigome_focus do |f|
|
160
|
+
f.fields ["b", "a"]
|
161
|
+
end
|
162
|
+
end
|
163
|
+
i = @model_class.new(:a => "abc", :b => "xyz")
|
164
|
+
expected = ActiveSupport::OrderedHash.new
|
165
|
+
expected["b"] = "xyz"
|
166
|
+
expected["a"] = "abc"
|
167
|
+
assert_equal expected, i.mochigome_focus.field_data
|
168
|
+
end
|
169
|
+
|
170
|
+
it "can specify fields with multiple calls" do
|
171
|
+
@model_class.class_eval do
|
172
|
+
acts_as_mochigome_focus do |f|
|
173
|
+
f.fields ["a"]
|
174
|
+
f.fields ["b"]
|
175
|
+
end
|
176
|
+
end
|
177
|
+
i = @model_class.new(:a => "abc", :b => "xyz")
|
178
|
+
expected = ActiveSupport::OrderedHash.new
|
179
|
+
expected["a"] = "abc"
|
180
|
+
expected["b"] = "xyz"
|
181
|
+
assert_equal expected, i.mochigome_focus.field_data
|
182
|
+
end
|
183
|
+
|
184
|
+
it "can specify fields with custom names" do
|
185
|
+
@model_class.class_eval do
|
186
|
+
acts_as_mochigome_focus do |f|
|
187
|
+
f.fields [{:Abraham => :a}, {:Barcelona => :b}]
|
188
|
+
end
|
189
|
+
end
|
190
|
+
i = @model_class.new(:a => "abc", :b => "xyz")
|
191
|
+
expected = ActiveSupport::OrderedHash.new
|
192
|
+
expected["Abraham"] = "abc"
|
193
|
+
expected["Barcelona"] = "xyz"
|
194
|
+
assert_equal expected, i.mochigome_focus.field_data
|
195
|
+
end
|
196
|
+
|
197
|
+
it "can specify fields with custom implementations" do
|
198
|
+
@model_class.class_eval do
|
199
|
+
acts_as_mochigome_focus do |f|
|
200
|
+
f.fields [{:concat => lambda {|obj| obj.a + obj.b}}]
|
201
|
+
end
|
202
|
+
end
|
203
|
+
i = @model_class.new(:a => "abc", :b => "xyz")
|
204
|
+
expected = ActiveSupport::OrderedHash.new
|
205
|
+
expected["concat"] = "abcxyz"
|
206
|
+
assert_equal expected, i.mochigome_focus.field_data
|
207
|
+
end
|
208
|
+
|
209
|
+
it "cannot call f.fields with nonsense" do
|
210
|
+
assert_raises Mochigome::ModelSetupError do
|
211
|
+
@model_class.class_eval do
|
212
|
+
acts_as_mochigome_focus do |f|
|
213
|
+
f.fields 123
|
214
|
+
end
|
215
|
+
end
|
216
|
+
end
|
217
|
+
assert_raises Mochigome::ModelSetupError do
|
218
|
+
@model_class.class_eval do
|
219
|
+
acts_as_mochigome_focus do |f|
|
220
|
+
f.fields [789]
|
221
|
+
end
|
222
|
+
end
|
223
|
+
end
|
224
|
+
end
|
225
|
+
|
226
|
+
[:name, :id, :type, :internal_type].each do |n|
|
227
|
+
it "cannot specify fields named the same as reserved term '#{n}'" do
|
228
|
+
assert_raises Mochigome::ModelSetupError do
|
229
|
+
@model_class.class_eval do
|
230
|
+
acts_as_mochigome_focus do |f|
|
231
|
+
f.fields [n]
|
232
|
+
end
|
233
|
+
end
|
234
|
+
end
|
235
|
+
assert_raises Mochigome::ModelSetupError do
|
236
|
+
@model_class.class_eval do
|
237
|
+
acts_as_mochigome_focus do |f|
|
238
|
+
f.fields [n.to_s.titleize]
|
239
|
+
end
|
240
|
+
end
|
241
|
+
end
|
242
|
+
assert_raises Mochigome::ModelSetupError do
|
243
|
+
@model_class.class_eval do
|
244
|
+
acts_as_mochigome_focus do |f|
|
245
|
+
f.fields [{n => :foo}]
|
246
|
+
end
|
247
|
+
end
|
248
|
+
end
|
249
|
+
end
|
250
|
+
end
|
251
|
+
|
252
|
+
it "appears in Mochigome's global model list if it acts_as_mochigome_focus" do
|
253
|
+
assert !Mochigome.reportFocusModels.include?(@model_class)
|
254
|
+
@model_class.class_eval do
|
255
|
+
acts_as_mochigome_focus
|
256
|
+
end
|
257
|
+
assert Mochigome.reportFocusModels.include?(@model_class)
|
258
|
+
end
|
259
|
+
|
260
|
+
it "can specify aggregated data to be collected" do
|
261
|
+
@model_class.class_eval do
|
262
|
+
has_mochigome_aggregations [:average_x, :Count, "sum x"]
|
263
|
+
end
|
264
|
+
# Peeking in past API to make sure it set the expressions correctly
|
265
|
+
assert_equal [
|
266
|
+
{:name => "Whales average x", :expr => "avg(x)"},
|
267
|
+
{:name => "Whales Count", :expr => "count()"},
|
268
|
+
{:name => "Whales sum x", :expr => "sum(x)"}
|
269
|
+
], @model_class.mochigome_aggregations
|
270
|
+
end
|
271
|
+
|
272
|
+
it "can specify aggregations with custom names" do
|
273
|
+
@model_class.class_eval do
|
274
|
+
has_mochigome_aggregations [{"Mean X" => "avg x"}]
|
275
|
+
end
|
276
|
+
assert_equal [
|
277
|
+
{:name => "Mean X", :expr => "avg(x)"}
|
278
|
+
], @model_class.mochigome_aggregations
|
279
|
+
end
|
280
|
+
|
281
|
+
it "can specify aggregations with custom SQL expressions" do
|
282
|
+
@model_class.class_eval do
|
283
|
+
has_mochigome_aggregations [{"The Answer" => "7*6"}]
|
284
|
+
end
|
285
|
+
assert_equal [
|
286
|
+
{:name => "The Answer", :expr => "7*6"}
|
287
|
+
], @model_class.mochigome_aggregations
|
288
|
+
end
|
289
|
+
|
290
|
+
it "can specify aggregations with custom conditions" do
|
291
|
+
@model_class.class_eval do
|
292
|
+
has_mochigome_aggregations [{"Blue Sales" => ["count", "color='blue'"]}]
|
293
|
+
end
|
294
|
+
assert_equal [
|
295
|
+
{:name => "Blue Sales", :expr => "count()", :conditions => "color='blue'"}
|
296
|
+
], @model_class.mochigome_aggregations
|
297
|
+
end
|
298
|
+
|
299
|
+
it "cannot call has_mochigome_aggregations with nonsense" do
|
300
|
+
assert_raises Mochigome::ModelSetupError do
|
301
|
+
@model_class.class_eval do
|
302
|
+
has_mochigome_aggregations 3
|
303
|
+
end
|
304
|
+
end
|
305
|
+
assert_raises Mochigome::ModelSetupError do
|
306
|
+
@model_class.class_eval do
|
307
|
+
has_mochigome_aggregations [42]
|
308
|
+
end
|
309
|
+
end
|
310
|
+
end
|
311
|
+
|
312
|
+
describe "with some aggregatable data" do
|
313
|
+
before do
|
314
|
+
@store1 = create(:store)
|
315
|
+
@store2 = create(:store)
|
316
|
+
@product_a = create(:product, :name => "Product A", :price => 30)
|
317
|
+
@product_b = create(:product, :name => "Product B", :price => 50)
|
318
|
+
@sp1A = create(:store_product, :store => @store1, :product => @product_a)
|
319
|
+
@sp1B = create(:store_product, :store => @store1, :product => @product_b)
|
320
|
+
@sp2A = create(:store_product, :store => @store2, :product => @product_a)
|
321
|
+
@sp2B = create(:store_product, :store => @store2, :product => @product_b)
|
322
|
+
[
|
323
|
+
[2, @sp1A],
|
324
|
+
[4, @sp1B],
|
325
|
+
[7, @sp2A],
|
326
|
+
[3, @sp2B]
|
327
|
+
].each do |num, sp|
|
328
|
+
num.times { create(:sale, :store_product => sp) }
|
329
|
+
end
|
330
|
+
end
|
331
|
+
|
332
|
+
it "can collect aggregate data from a report focus through an association" do
|
333
|
+
assert_equal 9, @product_a.mochigome_focus.aggregate_data('sales')['Sales count']
|
334
|
+
assert_equal 7, @product_b.mochigome_focus.aggregate_data('sales')['Sales count']
|
335
|
+
end
|
336
|
+
|
337
|
+
it "can collect aggregate data through all known associations with :all keyword" do
|
338
|
+
assert_equal 9, @product_a.mochigome_focus.aggregate_data(:all)['Sales count']
|
339
|
+
end
|
340
|
+
|
341
|
+
it "returns both field data and all aggregate data with the data method" do
|
342
|
+
data = @product_a.mochigome_focus.data
|
343
|
+
assert_equal 9, data['Sales count']
|
344
|
+
assert_equal 30, data['price']
|
345
|
+
end
|
346
|
+
|
347
|
+
it "can return data aggregated in the context of another class with similar assoc" do
|
348
|
+
focus = @product_a.mochigome_focus
|
349
|
+
assert_equal 2, focus.aggregate_data('sales', :context => [@sp1A])['Sales count']
|
350
|
+
end
|
351
|
+
|
352
|
+
it "can return data aggregated in the context through the data method" do
|
353
|
+
focus = @product_a.mochigome_focus
|
354
|
+
assert_equal 2, focus.data(:context => [@sp1A])['Sales count']
|
355
|
+
end
|
356
|
+
|
357
|
+
it "can return data aggregated using a custom sql expression" do
|
358
|
+
focus = @store1.mochigome_focus
|
359
|
+
assert_equal 9001, focus.data['Power level']
|
360
|
+
end
|
361
|
+
|
362
|
+
it "can return data aggregated using custom conditions" do
|
363
|
+
focus = @store1.mochigome_focus
|
364
|
+
assert_equal 1, focus.data['Expensive products']
|
365
|
+
end
|
366
|
+
end
|
367
|
+
end
|
@@ -0,0 +1,202 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/../test_helper')
|
2
|
+
|
3
|
+
describe Mochigome::Query do
|
4
|
+
before do
|
5
|
+
@category1 = create(:category, :name => "Category 1")
|
6
|
+
@product_a = create(:product, :name => "Product A", :category => @category1)
|
7
|
+
@product_b = create(:product, :name => "Product B", :category => @category1)
|
8
|
+
|
9
|
+
# Belongs to a category, but fails Category's has_many(:products) conditions
|
10
|
+
@product_x = create(:product, :name => "Product X", :category => @category1, :categorized => false)
|
11
|
+
|
12
|
+
@category2 = create(:category, :name => "Category 2")
|
13
|
+
@product_c = create(:product, :name => "Product C", :category => @category2)
|
14
|
+
@product_d = create(:product, :name => "Product D", :category => @category2)
|
15
|
+
|
16
|
+
@product_e = create(:product, :name => "Product E") # No category
|
17
|
+
|
18
|
+
@john = create(:owner, :first_name => "John", :last_name => "Smith")
|
19
|
+
@store_x = create(:store, :name => "John's Store", :owner => @john)
|
20
|
+
|
21
|
+
@jane = create(:owner, :first_name => "Jane", :last_name => "Doe")
|
22
|
+
@store_y = create(:store, :name => "Jane's Store (North)", :owner => @jane)
|
23
|
+
@store_z = create(:store, :name => "Jane's Store (South)", :owner => @jane)
|
24
|
+
|
25
|
+
@sp_xa = create(:store_product, :store => @store_x, :product => @product_a)
|
26
|
+
@sp_xc = create(:store_product, :store => @store_x, :product => @product_c)
|
27
|
+
@sp_ya = create(:store_product, :store => @store_y, :product => @product_a)
|
28
|
+
@sp_yb = create(:store_product, :store => @store_y, :product => @product_b)
|
29
|
+
@sp_ye = create(:store_product, :store => @store_y, :product => @product_e)
|
30
|
+
@sp_zc = create(:store_product, :store => @store_z, :product => @product_c)
|
31
|
+
@sp_zd = create(:store_product, :store => @store_z, :product => @product_d)
|
32
|
+
|
33
|
+
[
|
34
|
+
[@sp_xa, 5],
|
35
|
+
[@sp_xc, 3],
|
36
|
+
[@sp_ya, 4],
|
37
|
+
[@sp_yb, 6],
|
38
|
+
[@sp_ye, 1],
|
39
|
+
[@sp_zc, 2],
|
40
|
+
[@sp_zd, 3]
|
41
|
+
].each do |sp, n|
|
42
|
+
n.times{create(:sale, :store_product => sp)}
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
# Convenience function to check DataSet output validity
|
47
|
+
def assert_equal_objs(a, b)
|
48
|
+
assert_equal a.size, b.size
|
49
|
+
# Not checking aggregate data because we don't know about a's context here
|
50
|
+
a.zip(b).each do |obj, fields|
|
51
|
+
obj.mochigome_focus.field_data.each do |k,v|
|
52
|
+
assert_equal v, fields[k]
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
it "returns an empty DataNode if no objects given" do
|
58
|
+
q = Mochigome::Query.new([Category, Product])
|
59
|
+
data_node = q.run([])
|
60
|
+
assert_empty data_node
|
61
|
+
assert_empty data_node.children
|
62
|
+
end
|
63
|
+
|
64
|
+
it "can build a one-layer DataNode" do
|
65
|
+
q = Mochigome::Query.new([Product])
|
66
|
+
data_node = q.run(@product_a)
|
67
|
+
assert_equal_objs [@product_a], data_node.children
|
68
|
+
assert_empty data_node.children[0].children
|
69
|
+
end
|
70
|
+
|
71
|
+
it "uses the model focus's type name for the DataNode's type name" do
|
72
|
+
q = Mochigome::Query.new([Store])
|
73
|
+
data_node = q.run(@store_x)
|
74
|
+
assert_equal "Storefront", data_node.children[0].type_name.to_s
|
75
|
+
end
|
76
|
+
|
77
|
+
it "adds an internal_type attribute containing the model class's name" do
|
78
|
+
q = Mochigome::Query.new([Store])
|
79
|
+
data_node = q.run(@store_x)
|
80
|
+
assert_equal "Store", data_node.children[0][:internal_type]
|
81
|
+
end
|
82
|
+
|
83
|
+
it "can build a two-layer tree from a record with a belongs_to association" do
|
84
|
+
q = Mochigome::Query.new([Category, Product])
|
85
|
+
data_node = q.run(@product_a)
|
86
|
+
assert_equal_objs [@category1], data_node.children
|
87
|
+
assert_equal_objs [@product_a], data_node.children[0].children
|
88
|
+
assert_empty data_node.children[0].children[0].children
|
89
|
+
end
|
90
|
+
|
91
|
+
it "can build a two-layer tree from an array of records in the second layer" do
|
92
|
+
q = Mochigome::Query.new([Category, Product])
|
93
|
+
data_node = q.run([@product_a, @product_d, @product_b])
|
94
|
+
assert_equal_objs [@category1, @category2], data_node.children
|
95
|
+
assert_equal_objs [@product_a, @product_b], data_node.children[0].children
|
96
|
+
assert_equal_objs [@product_d], data_node.children[1].children
|
97
|
+
end
|
98
|
+
|
99
|
+
it "can build a two-layer tree from a record with a has_many association" do
|
100
|
+
q = Mochigome::Query.new([Category, Product])
|
101
|
+
data_node = q.run(@category1)
|
102
|
+
assert_equal_objs [@category1], data_node.children
|
103
|
+
assert_equal_objs [@product_a, @product_b], data_node.children[0].children
|
104
|
+
assert_empty data_node.children[0].children[0].children
|
105
|
+
end
|
106
|
+
|
107
|
+
it "cannot build a DataNode tree when given disconnected layers" do
|
108
|
+
q = Mochigome::Query.new([Category, BoringDatum])
|
109
|
+
assert_raises Mochigome::QueryError do
|
110
|
+
data_node = q.run(@category1)
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
it "can build a three-layer tree from any layer" do
|
115
|
+
q = Mochigome::Query.new([Owner, Store, Product])
|
116
|
+
[
|
117
|
+
[@john, @jane],
|
118
|
+
[@store_x, @store_y, @store_z],
|
119
|
+
[@product_a, @product_b, @product_c, @product_d, @product_e]
|
120
|
+
].each do |tgt|
|
121
|
+
data_node = q.run(tgt)
|
122
|
+
assert_equal_objs [@john, @jane], data_node.children
|
123
|
+
assert_equal_objs [@store_x], data_node.children[0].children
|
124
|
+
assert_equal_objs [@store_y, @store_z], data_node.children[1].children
|
125
|
+
assert_equal_objs [@product_a, @product_c],
|
126
|
+
data_node.children[0].children[0].children
|
127
|
+
assert_equal_objs [@product_c, @product_d],
|
128
|
+
data_node.children[1].children[1].children
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
it "collects aggregate data in the context of all layers when traversing down" do
|
133
|
+
q = Mochigome::Query.new([Owner, Store, Product])
|
134
|
+
data_node = q.run([@john, @jane])
|
135
|
+
# Store X, Product C
|
136
|
+
assert_equal "Product C", data_node.children[0].children[0].children[1].name
|
137
|
+
assert_equal 3, data_node.children[0].children[0].children[1]['Sales count']
|
138
|
+
# Store Z, Product C
|
139
|
+
assert_equal "Product C", data_node.children[1].children[1].children[0].name
|
140
|
+
assert_equal 2, data_node.children[1].children[1].children[0]['Sales count']
|
141
|
+
end
|
142
|
+
|
143
|
+
it "collects aggregate data in the context of all layers when traversing up" do
|
144
|
+
q = Mochigome::Query.new([Owner, Store, Product])
|
145
|
+
data_node = q.run(@product_c)
|
146
|
+
# Store X, Product C
|
147
|
+
assert_equal "Product C", data_node.children[0].children[0].children[0].name
|
148
|
+
assert_equal 3, data_node.children[0].children[0].children[0]['Sales count']
|
149
|
+
# Store Z, Product C
|
150
|
+
assert_equal "Product C", data_node.children[1].children[0].children[0].name
|
151
|
+
assert_equal 2, data_node.children[1].children[0].children[0]['Sales count']
|
152
|
+
end
|
153
|
+
|
154
|
+
it "collects aggregate data in the context of distant layers" do
|
155
|
+
# TODO: Implement me! I think this is necessary to justify focus_data_node_objs passing obj_stack
|
156
|
+
end
|
157
|
+
|
158
|
+
it "puts a comment on the root node describing the query" do
|
159
|
+
q = Mochigome::Query.new([Owner, Store, Product])
|
160
|
+
data_node = q.run([@store_x, @store_y, @store_z])
|
161
|
+
c = data_node.comment
|
162
|
+
assert_match c, /^Mochigome Version: #{Mochigome::VERSION}\n/
|
163
|
+
assert_match c, /\nTime: \w{3} \w{3} \d+ .+\n/
|
164
|
+
assert_match c, /\nLayers: Owner => Store => Product\n/
|
165
|
+
assert_match c, /\nAR Association Path:\n\* <- Owner.+\n\* == Store.+\n\* -> Product.+\n/
|
166
|
+
end
|
167
|
+
|
168
|
+
it "puts a descriptive comment on the first node of each layer" do
|
169
|
+
q = Mochigome::Query.new([Owner, Store, Product])
|
170
|
+
data_node = q.run([@store_x, @store_y, @store_z])
|
171
|
+
|
172
|
+
owner_comment = data_node.children[0].comment
|
173
|
+
assert owner_comment
|
174
|
+
assert_nil data_node.children[1].comment # No comment on second owner
|
175
|
+
|
176
|
+
store_comment = data_node.children[0].children[0].comment
|
177
|
+
assert store_comment
|
178
|
+
assert_nil data_node.children[1].children[0].comment # No comment on second store
|
179
|
+
|
180
|
+
product_comment = data_node.children[0].children[0].children[0].comment
|
181
|
+
assert product_comment
|
182
|
+
assert_nil data_node.children[0].children[0].children[1].comment # No comment on 2nd product
|
183
|
+
|
184
|
+
[owner_comment, store_comment, product_comment].each do |comment|
|
185
|
+
assert_match comment, /^Context:\nOwner:#{@john.id}.*\n/ # Owner is always in context
|
186
|
+
end
|
187
|
+
end
|
188
|
+
|
189
|
+
it "will not allow a query on targets of different types" do
|
190
|
+
q = Mochigome::Query.new([Owner, Store, Product])
|
191
|
+
assert_raises Mochigome::QueryError do
|
192
|
+
q.run([@store_x, @john])
|
193
|
+
end
|
194
|
+
end
|
195
|
+
|
196
|
+
it "will not allow a query on targets not in the layer list" do
|
197
|
+
q = Mochigome::Query.new([Product])
|
198
|
+
assert_raises Mochigome::QueryError do
|
199
|
+
q.run(@category1)
|
200
|
+
end
|
201
|
+
end
|
202
|
+
end
|
metadata
ADDED
@@ -0,0 +1,143 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: mochigome
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
hash: 27
|
5
|
+
prerelease:
|
6
|
+
segments:
|
7
|
+
- 0
|
8
|
+
- 0
|
9
|
+
- 2
|
10
|
+
version: 0.0.2
|
11
|
+
platform: ruby
|
12
|
+
authors:
|
13
|
+
- David Mike Simon
|
14
|
+
autorequire:
|
15
|
+
bindir: bin
|
16
|
+
cert_chain: []
|
17
|
+
|
18
|
+
date: 2011-11-14 00:00:00 Z
|
19
|
+
dependencies:
|
20
|
+
- !ruby/object:Gem::Dependency
|
21
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
22
|
+
none: false
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
hash: 3
|
27
|
+
segments:
|
28
|
+
- 0
|
29
|
+
version: "0"
|
30
|
+
version_requirements: *id001
|
31
|
+
name: ruport
|
32
|
+
prerelease: false
|
33
|
+
type: :runtime
|
34
|
+
- !ruby/object:Gem::Dependency
|
35
|
+
requirement: &id002 !ruby/object:Gem::Requirement
|
36
|
+
none: false
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
hash: 3
|
41
|
+
segments:
|
42
|
+
- 0
|
43
|
+
version: "0"
|
44
|
+
version_requirements: *id002
|
45
|
+
name: nokogiri
|
46
|
+
prerelease: false
|
47
|
+
type: :runtime
|
48
|
+
- !ruby/object:Gem::Dependency
|
49
|
+
requirement: &id003 !ruby/object:Gem::Requirement
|
50
|
+
none: false
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
hash: 3
|
55
|
+
segments:
|
56
|
+
- 0
|
57
|
+
version: "0"
|
58
|
+
version_requirements: *id003
|
59
|
+
name: rgl
|
60
|
+
prerelease: false
|
61
|
+
type: :runtime
|
62
|
+
description: Mochigome builds sophisticated report datasets from your ActiveRecord models
|
63
|
+
email: david.mike.simon@gmail.com
|
64
|
+
executables: []
|
65
|
+
|
66
|
+
extensions: []
|
67
|
+
|
68
|
+
extra_rdoc_files: []
|
69
|
+
|
70
|
+
files:
|
71
|
+
- .autotest
|
72
|
+
- Gemfile
|
73
|
+
- Gemfile.lock
|
74
|
+
- LICENSE
|
75
|
+
- README.rdoc
|
76
|
+
- Rakefile
|
77
|
+
- TODO
|
78
|
+
- lib/data_node.rb
|
79
|
+
- lib/exceptions.rb
|
80
|
+
- lib/mochigome.rb
|
81
|
+
- lib/mochigome_ver.rb
|
82
|
+
- lib/model_extensions.rb
|
83
|
+
- lib/query.rb
|
84
|
+
- test/app_root/app/controllers/application_controller.rb
|
85
|
+
- test/app_root/app/controllers/owners_controller.rb
|
86
|
+
- test/app_root/app/models/boring_datum.rb
|
87
|
+
- test/app_root/app/models/category.rb
|
88
|
+
- test/app_root/app/models/owner.rb
|
89
|
+
- test/app_root/app/models/product.rb
|
90
|
+
- test/app_root/app/models/sale.rb
|
91
|
+
- test/app_root/app/models/store.rb
|
92
|
+
- test/app_root/app/models/store_product.rb
|
93
|
+
- test/app_root/config/boot.rb
|
94
|
+
- test/app_root/config/database-pg.yml
|
95
|
+
- test/app_root/config/database.yml
|
96
|
+
- test/app_root/config/environment.rb
|
97
|
+
- test/app_root/config/environments/test.rb
|
98
|
+
- test/app_root/config/offroad.yml
|
99
|
+
- test/app_root/config/preinitializer.rb
|
100
|
+
- test/app_root/config/routes.rb
|
101
|
+
- test/app_root/db/migrate/20110817163830_create_tables.rb
|
102
|
+
- test/app_root/vendor/plugins/mochigome/init.rb
|
103
|
+
- test/factories.rb
|
104
|
+
- test/test.watchr
|
105
|
+
- test/test_helper.rb
|
106
|
+
- test/unit/data_node_test.rb
|
107
|
+
- test/unit/model_extensions_test.rb
|
108
|
+
- test/unit/query_test.rb
|
109
|
+
homepage: http://github.com/DavidMikeSimon/mochigome
|
110
|
+
licenses: []
|
111
|
+
|
112
|
+
post_install_message:
|
113
|
+
rdoc_options: []
|
114
|
+
|
115
|
+
require_paths:
|
116
|
+
- lib
|
117
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
118
|
+
none: false
|
119
|
+
requirements:
|
120
|
+
- - ">="
|
121
|
+
- !ruby/object:Gem::Version
|
122
|
+
hash: 3
|
123
|
+
segments:
|
124
|
+
- 0
|
125
|
+
version: "0"
|
126
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
127
|
+
none: false
|
128
|
+
requirements:
|
129
|
+
- - ">="
|
130
|
+
- !ruby/object:Gem::Version
|
131
|
+
hash: 3
|
132
|
+
segments:
|
133
|
+
- 0
|
134
|
+
version: "0"
|
135
|
+
requirements: []
|
136
|
+
|
137
|
+
rubyforge_project: "[none]"
|
138
|
+
rubygems_version: 1.8.6
|
139
|
+
signing_key:
|
140
|
+
specification_version: 3
|
141
|
+
summary: User-customizable report generator
|
142
|
+
test_files: []
|
143
|
+
|