mochigome 0.2.2 → 0.2.3

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.
@@ -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