mochigome 0.0.2
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/.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
|
+
|