mochigome 0.2.2 → 0.2.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,3 +1,3 @@
1
1
  module Mochigome
2
- VERSION = "0.2.2"
2
+ VERSION = "0.2.3"
3
3
  end
@@ -189,6 +189,7 @@ module Mochigome
189
189
  @options[:custom_subgroup_exprs] = ActiveSupport::OrderedHash.new
190
190
  @options[:custom_assocs] = ActiveSupport::OrderedHash.new
191
191
  @options[:ignore_assocs] = Set.new
192
+ @options[:preferred_paths] = {}
192
193
  end
193
194
 
194
195
  def type_name(n)
@@ -247,6 +248,10 @@ module Mochigome
247
248
  def ignore_assoc(name)
248
249
  @options[:ignore_assocs].add name.to_sym
249
250
  end
251
+
252
+ def preferred_path_to(target_model, assoc_name)
253
+ @options[:preferred_paths][target_model.name] = assoc_name
254
+ end
250
255
  end
251
256
 
252
257
  class AggregationSettings
@@ -120,7 +120,8 @@ module Mochigome
120
120
 
121
121
  ignore_assocs = []
122
122
  if model.acts_as_mochigome_focus?
123
- ignore_assocs = model.mochigome_focus_settings.options[:ignore_assocs]
123
+ opts = model.mochigome_focus_settings.options
124
+ ignore_assocs = opts[:ignore_assocs]
124
125
  end
125
126
 
126
127
  model.reflections.
@@ -157,12 +158,6 @@ module Mochigome
157
158
  end
158
159
 
159
160
  added_models.each do |model|
160
- # FIXME: Un-DRY, this is a C&P from above
161
- ignore_assocs = []
162
- if model.acts_as_mochigome_focus?
163
- ignore_assocs = model.mochigome_focus_settings.options[:ignore_assocs]
164
- end
165
-
166
161
  next unless @assoc_graph.has_vertex?(model)
167
162
  path_tree = @assoc_graph.bfs_search_tree_from(model).reverse
168
163
  path_tree.depth_first_search do |tgt_model|
@@ -173,10 +168,22 @@ module Mochigome
173
168
  end
174
169
  @shortest_paths[[model,tgt_model]] = path
175
170
  end
171
+ end
172
+
173
+ added_models.each do |model|
174
+ ignore_assocs, model_preferred_paths = [], {}
175
+ if model.acts_as_mochigome_focus?
176
+ opts = model.mochigome_focus_settings.options
177
+ ignore_assocs = opts[:ignore_assocs]
178
+ model_preferred_paths = opts[:preferred_paths]
179
+ end
180
+
181
+ preferred_paths = {}
176
182
 
177
183
  # Use through reflections as a hint for preferred indirect paths
178
- model.reflections.
179
- select{|name, assoc| assoc.through_reflection}.
184
+ # TODO Support for nested through reflections
185
+ # (Though Rails 2 doesn't support them either...)
186
+ model.reflections.select{|name, assoc| assoc.through_reflection}.
180
187
  reject{|name, assoc| ignore_assocs.include? name.to_sym}.
181
188
  reject{|name, assoc| ignore_assocs.include? assoc.through_reflection.name.to_sym}.
182
189
  to_a.sort{|a,b| a.first.to_s <=> b.first.to_s}.
@@ -187,9 +194,60 @@ module Mochigome
187
194
  rescue NameError
188
195
  # FIXME Can't handle polymorphic through reflection
189
196
  end
190
- edge = [model,foreign_model]
191
- next if @shortest_paths[edge].try(:size).try(:<, 3)
192
- @shortest_paths[edge] = [model, join_model, foreign_model]
197
+ edge = [model, foreign_model]
198
+ path = [model, join_model, foreign_model]
199
+ next if @shortest_paths[edge].try(:size).try(:<, path.size)
200
+ preferred_paths[edge] = path
201
+ end
202
+
203
+ # Model focus can specify paths with prefered_path_to
204
+ model_preferred_paths.each do |tgt_model_name, assoc_name|
205
+ tgt_model = tgt_model_name.constantize
206
+ edge = [model, tgt_model]
207
+ assoc = model.reflections[assoc_name]
208
+ sub_path = @shortest_paths[[assoc.klass, tgt_model]]
209
+ unless sub_path
210
+ raise ModelSetupError.new(
211
+ "Can't find subpath to #{tgt_model} via #{model.name}.#{assoc_name}"
212
+ )
213
+ end
214
+ if sub_path.include?(model)
215
+ raise ModelSetupError.new(
216
+ "Subpath to #{tgt_model} via #{model.name}.#{assoc_name} loops back"
217
+ )
218
+ end
219
+ sub_link = @shortest_paths[[model, assoc.klass]]
220
+ preferred_paths[edge] = sub_link + sub_path.drop(1)
221
+ end
222
+
223
+ # Replace all instances of the default path in the path directory
224
+ # with the preferred path, including when the default path is
225
+ # a subset of a larger path, and/or when the direction of travel
226
+ # is reversed.
227
+ # FIXME What if preferred paths conflict?
228
+ # FIXME What if one preferred path causes a shortest_path to become
229
+ # applicable under another one? Then arbitrary model scanning
230
+ # order matters, and it shouldn't. Is there even a consistent
231
+ # way to deal with this?
232
+ preferred_paths.each do |edge, path|
233
+ [lambda{|a| a}, lambda{|a| a.reverse}].each do |prc|
234
+ e, p = prc.call(edge), prc.call(path)
235
+ old_path = @shortest_paths[e]
236
+ next if old_path == p
237
+ edges_to_replace = {}
238
+ @shortest_paths.each do |se, sp|
239
+ p_begin = sp.find_index(old_path.first)
240
+ if p_begin && sp[p_begin, old_path.size] == old_path
241
+ edges_to_replace[se] =
242
+ sp.take(p_begin) +
243
+ p +
244
+ sp.drop(p_begin + old_path.size)
245
+ end
246
+ end
247
+ edges_to_replace.each do |re, rp|
248
+ @shortest_paths[re] = rp
249
+ end
250
+ end
193
251
  end
194
252
  end
195
253
  end
@@ -51,8 +51,8 @@ module Mochigome
51
51
  end
52
52
  end
53
53
 
54
- raise QueryError.new("No path to #{model}") unless best_path
55
- join_on_path(best_path)
54
+ raise QueryError.new("No path to #{model} from #{@models.map(&:name).inspect}") unless best_path
55
+ join_on_path best_path, "Best path to model #{model}"
56
56
 
57
57
  # Also use the conditions of any other unique path
58
58
  # TODO: Write a test that requires the below code to work
@@ -60,22 +60,22 @@ module Mochigome
60
60
  extra_path = @model_graph.path_thru([n, model])
61
61
  if extra_path
62
62
  unless best_path.all?{|m| extra_path.include?(m)}
63
- join_on_path extra_path
63
+ join_on_path extra_path, "Additional path to model #{model}"
64
64
  end
65
65
  end
66
66
  end
67
67
  end
68
68
 
69
- def join_on_path_thru(path)
69
+ def join_on_path_thru(path, descrip = nil)
70
70
  full_path = @model_graph.path_thru(path)
71
71
  if full_path
72
- join_on_path(full_path)
72
+ join_on_path(full_path, descrip || "Generic path thru #{path.map(&:name).inspect}")
73
73
  else
74
74
  raise QueryError.new("Cannot route thru #{path.map(&:name).inspect}")
75
75
  end
76
76
  end
77
77
 
78
- def join_on_path(path, options = {})
78
+ def join_on_path(path, descrip = "Generic")
79
79
  begin
80
80
  path = path.map(&:to_real_model).uniq
81
81
  join_to_model path.first
@@ -89,7 +89,7 @@ module Mochigome
89
89
  join_descrips << tgt.name
90
90
  end
91
91
  end
92
- @join_path_descriptions << join_descrips.join("->")
92
+ @join_path_descriptions << "#{join_descrips.join("->")} (#{descrip})"
93
93
  rescue QueryError => e
94
94
  raise QueryError.new("Error pathing #{path.map(&:name).inspect}: #{e}")
95
95
  end
@@ -130,7 +130,7 @@ module Mochigome
130
130
  # FIXME: Eventually we need to support joins that
131
131
  # double back, if only for CanCan stuff, so get rid of this
132
132
  # uniq junk.
133
- join_on_path_thru path.uniq
133
+ join_on_path_thru path.uniq, "Access filter for #{m.name}"
134
134
  end
135
135
  if h[:condition]
136
136
  apply_condition h.delete(:condition)
@@ -0,0 +1,6 @@
1
+ class Favorite < ActiveRecord::Base
2
+ acts_as_mochigome_focus
3
+
4
+ belongs_to :owner
5
+ belongs_to :product
6
+ end
@@ -1,9 +1,11 @@
1
1
  class Owner < ActiveRecord::Base
2
2
  acts_as_mochigome_focus do |f|
3
3
  f.fieldset :age, ["birth_date", "age"]
4
+ f.preferred_path_to Product, :stores
4
5
  end
5
6
 
6
7
  has_many :stores
8
+ has_many :favorites
7
9
 
8
10
  def name(reverse = false)
9
11
  if reverse
@@ -33,6 +33,7 @@ class Product < ActiveRecord::Base
33
33
  has_many :store_products
34
34
  has_many :stores, :through => :store_products
35
35
  has_many :sales, :through => :store_products
36
+ has_many :favorites
36
37
 
37
38
  validates_presence_of :name
38
39
  validates_presence_of :price
@@ -38,6 +38,12 @@ class CreateTables < ActiveRecord::Migration
38
38
  t.timestamps
39
39
  end
40
40
 
41
+ # Used to create an alternate path to try and confuse ModelGraph
42
+ create_table :favorites do |t|
43
+ t.integer :owner_id
44
+ t.integer :product_id
45
+ end
46
+
41
47
  # Used by ModelExtensionTest to create temporary models
42
48
  create_table :fake do |t|
43
49
  t.timestamps
@@ -33,6 +33,11 @@ FactoryGirl.define do
33
33
  store_product
34
34
  end
35
35
 
36
+ factory :favorite do
37
+ owner
38
+ product
39
+ end
40
+
36
41
  factory :boring_datum do
37
42
  foo 'Bar'
38
43
  end
@@ -41,6 +41,11 @@ describe Mochigome::Query do
41
41
  n.times{create(:sale, :store_product => sp)}
42
42
  end
43
43
 
44
+ create(:favorite, :owner => @john, :product => @product_c)
45
+ create(:favorite, :owner => @john, :product => @product_d)
46
+ create(:favorite, :owner => @jane, :product => @product_c)
47
+ create(:favorite, :owner => @jane, :product => @product_a)
48
+
44
49
  (1..5).each do |div_num|
45
50
  WidgetDivisor.create(:divisor => div_num)
46
51
  end
@@ -318,6 +323,20 @@ describe Mochigome::Query do
318
323
  assert_empty data_node.children
319
324
  end
320
325
 
326
+ it "can collect aggregate data in non-association-order layer designs" do
327
+ q = Mochigome::Query.new([Product, Owner, Store], :aggregate_sources => [Sale])
328
+ data_node = q.run
329
+ assert_equal_children [
330
+ @product_a, @product_b, @product_c, @product_d, @product_e
331
+ ], data_node
332
+ assert_equal_children [@john, @jane], data_node/0
333
+ assert_equal_children [@jane], data_node/1
334
+ assert_equal_children [@store_x], data_node/0/0
335
+ assert_equal_children [@store_y], data_node/1/0
336
+ assert_equal 9, (data_node/0)["Sales count"]
337
+ assert_equal 5, (data_node/0/0)["Sales count"]
338
+ end
339
+
321
340
  it "can use a named aggregate data setting" do
322
341
  q = Mochigome::Query.new(
323
342
  [Owner],
@@ -531,7 +550,7 @@ describe Mochigome::Query do
531
550
 
532
551
  # TODO: Test use of non-trivial function for aggregation value
533
552
 
534
- it "puts a comment on the root node describing the query" do
553
+ it "sets a comment on the root node describing the query" do
535
554
  q = Mochigome::Query.new([Owner, Store, Product])
536
555
  data_node = q.run([@store_x, @store_y, @store_z])
537
556
  c = data_node.comment
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: mochigome
3
3
  version: !ruby/object:Gem::Version
4
- hash: 19
4
+ hash: 17
5
5
  prerelease:
6
6
  segments:
7
7
  - 0
8
8
  - 2
9
- - 2
10
- version: 0.2.2
9
+ - 3
10
+ version: 0.2.3
11
11
  platform: ruby
12
12
  authors:
13
13
  - David Mike Simon
@@ -15,7 +15,7 @@ autorequire:
15
15
  bindir: bin
16
16
  cert_chain: []
17
17
 
18
- date: 2012-10-03 00:00:00 Z
18
+ date: 2012-10-25 00:00:00 Z
19
19
  dependencies:
20
20
  - !ruby/object:Gem::Dependency
21
21
  version_requirements: &id001 !ruby/object:Gem::Requirement
@@ -121,6 +121,7 @@ files:
121
121
  - test/app_root/app/helpers/application_helper.rb
122
122
  - test/app_root/app/models/boring_datum.rb
123
123
  - test/app_root/app/models/category.rb
124
+ - test/app_root/app/models/favorite.rb
124
125
  - test/app_root/app/models/owner.rb
125
126
  - test/app_root/app/models/product.rb
126
127
  - test/app_root/app/models/sale.rb