acts_as_dag 1.1.2 → 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -16,14 +16,14 @@ module ActsAsDAG
16
16
  class_eval <<-EOV
17
17
  class ::#{options[:link_class]} < ActsAsDAG::AbstractLink
18
18
  self.table_name = '#{options[:link_table]}'
19
- belongs_to :parent, :class_name => '#{self.name}', :foreign_key => 'parent_id'
20
- belongs_to :child, :class_name => '#{self.name}', :foreign_key => 'child_id'
19
+ belongs_to :parent, :class_name => '#{self.name}', :foreign_key => :parent_id
20
+ belongs_to :child, :class_name => '#{self.name}', :foreign_key => :child_id
21
21
  end
22
22
 
23
23
  class ::#{options[:descendant_class]} < ActsAsDAG::AbstractDescendant
24
24
  self.table_name = '#{options[:descendant_table]}'
25
- belongs_to :ancestor, :class_name => '#{self.name}', :foreign_key => "ancestor_id"
26
- belongs_to :descendant, :class_name => '#{self.name}', :foreign_key => "descendant_id"
25
+ belongs_to :ancestor, :class_name => '#{self.name}', :foreign_key => :ancestor_id
26
+ belongs_to :descendant, :class_name => '#{self.name}', :foreign_key => :descendant_id
27
27
  end
28
28
 
29
29
  def self.link_class
@@ -45,32 +45,35 @@ module ActsAsDAG
45
45
  descendant_class.where(acts_as_dag_options[:link_conditions])
46
46
  end
47
47
 
48
-
49
48
  # Ancestors and descendants returned *include* self, e.g. A's descendants are [A,B,C,D]
50
49
  # Ancestors must always be returned in order of most distant to least
51
50
  # Descendants must always be returned in order of least distant to most
52
- # NOTE: multiple instances of the same descendant/ancestor may be returned if there are multiple paths from ancestor to descendant
51
+ # NOTE: Uniq in order to prevent multiple instance being returned if there are multiple paths between ancestor and descendant
53
52
  # A
54
53
  # / \
55
54
  # B C
56
55
  # \ /
57
56
  # D
58
57
  #
59
- has_many :ancestor_links, :class_name => descendant_class, :foreign_key => 'descendant_id', :conditions => options[:link_conditions], :dependent => :delete_all
60
- has_many :descendant_links, :class_name => descendant_class, :foreign_key => 'ancestor_id', :conditions => options[:link_conditions], :dependent => :delete_all
61
- has_many :ancestors, :through => :ancestor_links, :source => :ancestor, :order => "distance DESC"
62
- has_many :descendants, :through => :descendant_links, :source => :descendant, :order => "distance ASC"
58
+ has_many :ancestors, lambda { select("#{table_name}.*, #{descendant_class.table_name}.distance").order('distance DESC').uniq }, :through => :ancestor_links, :source => :ancestor
59
+ has_many :descendants, lambda { select("#{table_name}.*, #{descendant_class.table_name}.distance").order('distance ASC').uniq }, :through => :descendant_links, :source => :descendant
60
+
61
+ has_many :ancestor_links, lambda { where options[:link_conditions] }, :class_name => descendant_class, :foreign_key => 'descendant_id', :dependent => :delete_all
62
+ has_many :descendant_links, lambda { where options[:link_conditions] }, :class_name => descendant_class, :foreign_key => 'ancestor_id', :dependent => :delete_all
63
+
64
+ has_many :parent_links, lambda { where options[:link_conditions] }, :class_name => link_class, :foreign_key => 'child_id', :dependent => :delete_all
65
+ has_many :child_links, lambda { where options[:link_conditions] }, :class_name => link_class, :foreign_key => 'parent_id', :dependent => :delete_all
63
66
 
64
- has_many :parent_links, :class_name => link_class, :foreign_key => 'child_id', :conditions => options[:link_conditions], :dependent => :delete_all
65
- has_many :child_links, :class_name => link_class, :foreign_key => 'parent_id', :conditions => options[:link_conditions], :dependent => :delete_all
66
- has_many :parents, :through => :parent_links, :source => :parent
67
- has_many :children, :through => :child_links, :source => :child
67
+ has_many :parents, :through => :parent_links, :source => :parent
68
+ has_many :children, :through => :child_links, :source => :child
69
+
70
+ # NOTE: Use select to prevent ActiveRecord::ReadOnlyRecord if the returned records are modified
71
+ scope :roots, lambda { select("#{table_name}.*").joins(:parent_links).where(link_class.table_name => {:parent_id => nil}) }
72
+ scope :children, lambda { select("#{table_name}.*").joins(:parent_links).where.not(link_class.table_name => {:parent_id => nil}).uniq }
68
73
 
69
74
  after_create :initialize_links
70
75
  after_create :initialize_descendants
71
76
 
72
- scope :roots, joins(:parent_links).where(link_class.table_name => {:parent_id => nil})
73
-
74
77
  extend ActsAsDAG::ClassMethods
75
78
  include ActsAsDAG::InstanceMethods
76
79
  end
@@ -98,19 +101,21 @@ module ActsAsDAG
98
101
 
99
102
  # Try drop each category into each root
100
103
  categories.sort_by(&:name).each do |category|
104
+ start = Time.now
101
105
  suitable_parent = false
102
106
  roots_categories.each do |root|
103
107
  suitable_parent = true if ActsAsDAG::HelperMethods.plinko(root, category)
104
108
  end
105
109
  unless suitable_parent
106
- ActiveRecord::Base.logger.info "Plinko couldn't find a suitable parent for #{category.name} in #{categories.collect(&:name).join(', ')}"
110
+ ActiveRecord::Base.logger.info { "Plinko couldn't find a suitable parent for #{category.name}" }
107
111
  categories_with_no_parents << category
108
112
  end
113
+ puts "took #{Time.now - start} to analyze #{category.name}"
109
114
  end
110
115
 
111
116
  # Add all categories from this group without suitable parents to the roots
112
117
  if categories_with_no_parents.present?
113
- ActiveRecord::Base.logger.info "Adding #{categories_with_no_parents.collect(&:name).join(', ')} to roots"
118
+ ActiveRecord::Base.logger.info { "Adding #{categories_with_no_parents.collect(&:name).join(', ')} to roots" }
114
119
  roots_categories.concat categories_with_no_parents
115
120
  end
116
121
  end
@@ -121,10 +126,10 @@ module ActsAsDAG
121
126
  def reset_hierarchy(categories_to_reset = self.all)
122
127
  ids = categories_to_reset.collect(&:id)
123
128
 
124
- ActiveRecord::Base.logger.info "Clearing #{self.name} hierarchy links"
129
+ ActiveRecord::Base.logger.info { "Clearing #{self.name} hierarchy links" }
125
130
  link_table_entries.where("parent_id IN (?) OR child_id IN (?)", ids, ids).delete_all
126
131
 
127
- ActiveRecord::Base.logger.info "Clearing #{self.name} hierarchy descendants"
132
+ ActiveRecord::Base.logger.info { "Clearing #{self.name} hierarchy descendants" }
128
133
  descendant_table_entries.where("descendant_id IN (?) OR ancestor_id IN (?)", ids, ids).delete_all
129
134
 
130
135
  categories_to_reset.each do |category|
@@ -200,17 +205,17 @@ module ActsAsDAG
200
205
  # Searches all descendants for the best parent for the other
201
206
  # i.e. it lets you drop the category in at the top and it drops down the list until it finds its final resting place
202
207
  def self.plinko(current, other)
203
- ActiveRecord::Base.logger.info "Plinkoing '#{other.name}' into '#{current.name}'..."
208
+ # ActiveRecord::Base.logger.info { "Plinkoing '#{other.name}' into '#{current.name}'..." }
204
209
  if should_descend_from?(current, other)
205
210
  # Find the descendants of the current category that +other+ should descend from
206
- descendants_other_should_descend_from = current.descendants.select{|descendant| should_descend_from?(descendant, other)}
211
+ descendants_other_should_descend_from = current.descendants.select{|descendant| should_descend_from?(descendant, other) }
207
212
  # Of those, find the categories with the most number of matching words and make +other+ their child
208
213
  # We find all suitable candidates to provide support for categories whose names are permutations of each other
209
214
  # e.g. 'goat wool fibre' should be a child of 'goat wool' and 'wool goat' if both are present under 'goat'
210
215
  new_parents_group = descendants_other_should_descend_from.group_by{|category| matching_word_count(other, category)}.sort.reverse.first
211
216
  if new_parents_group.present?
212
217
  for new_parent in new_parents_group[1]
213
- ActiveRecord::Base.logger.info " '#{other.name}' landed under '#{new_parent.name}'"
218
+ ActiveRecord::Base.logger.info { " '#{other.name}' landed under '#{new_parent.name}'" }
214
219
  other.add_parent(new_parent)
215
220
 
216
221
  # We've just affected the associations in ways we can not possibly imagine, so let's clear the association cache
@@ -270,7 +275,7 @@ module ActsAsDAG
270
275
  # creates a single link in the given link_class's link table between parent and
271
276
  # child object ids and creates the appropriate entries in the descendant table
272
277
  def self.link(parent, child)
273
- # ActiveRecord::Base.logger.info "link(hierarchy_link_table = #{child.link_class}, hierarchy_descendant_table = #{child.descendant_class}, parent = #{parent.name}, child = #{child.name})"
278
+ # ActiveRecord::Base.logger.info { "link(hierarchy_link_table = #{child.link_class}, hierarchy_descendant_table = #{child.descendant_class}, parent = #{parent.name}, child = #{child.name})" }
274
279
 
275
280
  # Sanity check
276
281
  raise "Parent has no ID" if parent.id.nil?
@@ -282,7 +287,7 @@ module ActsAsDAG
282
287
  # Create a new parent-child link
283
288
  # Return if the link already exists because we can assume that the proper descendants already exist too
284
289
  if klass.link_table_entries.where(:parent_id => parent.id, :child_id => child.id).exists?
285
- ActiveRecord::Base.logger.info "Skipping #{child.descendant_class} update because the link already exists"
290
+ ActiveRecord::Base.logger.info { "Skipping #{child.descendant_class} update because the link already exists" }
286
291
  return
287
292
  else
288
293
  klass.link_table_entries.create!(:parent_id => parent.id, :child_id => child.id)
@@ -300,7 +305,7 @@ module ActsAsDAG
300
305
  child_descendant_links = klass.descendant_table_entries.where(:ancestor_id => child.id) # (totem pole model => totem pole model)
301
306
  for parent_ancestor_link in parent_ancestor_links
302
307
  for child_descendant_link in child_descendant_links
303
- klass.descendant_table_entries.where(:ancestor_id => parent_ancestor_link.ancestor_id, :descendant_id => child_descendant_link.descendant_id, :distance => parent_ancestor_link.distance + child_descendant_link.distance + 1).first_or_create!
308
+ klass.descendant_table_entries.find_or_create_by!(:ancestor_id => parent_ancestor_link.ancestor_id, :descendant_id => child_descendant_link.descendant_id, :distance => parent_ancestor_link.distance + child_descendant_link.distance + 1)
304
309
  end
305
310
  end
306
311
  end
@@ -309,7 +314,7 @@ module ActsAsDAG
309
314
  # child object id. Updates the appropriate Descendants table entries
310
315
  def self.unlink(parent, child)
311
316
  descendant_table_string = child.descendant_class.to_s
312
- # ActiveRecord::Base.logger.info "unlink(hierarchy_link_table = #{child.link_class}, hierarchy_descendant_table = #{descendant_table_string}, parent = #{parent ? parent.name : 'nil'}, child = #{child.name})"
317
+ # ActiveRecord::Base.logger.info { "unlink(hierarchy_link_table = #{child.link_class}, hierarchy_descendant_table = #{descendant_table_string}, parent = #{parent ? parent.name : 'nil'}, child = #{child.name})" }
313
318
 
314
319
  # Raise an exception if there is no child
315
320
  raise "Child cannot be nil when deleting a category_link" unless child
@@ -317,7 +322,7 @@ module ActsAsDAG
317
322
  klass = child.class
318
323
 
319
324
  # delete the links
320
- klass.link_table_entries.where(:parent_id => (parent ? parent.id : nil), :child_id => child.id).delete_all
325
+ klass.link_table_entries.where(:parent_id => parent.try(:id), :child_id => child.id).delete_all
321
326
 
322
327
  # If the parent was nil, we don't need to update descendants because there are no descendants of nil
323
328
  return unless parent
@@ -358,7 +363,7 @@ module ActsAsDAG
358
363
  # Create descendant links to each ancestor in the array (including itself)
359
364
  ancestors.reverse.each_with_index do |ancestor, index|
360
365
  ActiveRecord::Base.logger.info {"#{indent}#{ancestor.name} is an ancestor of #{current.name} with distance #{index}"}
361
- klass.descendant_table_entries.where(:ancestor_id => ancestor.id, :descendant_id => current.id, :distance => index).first_or_create!
366
+ klass.descendant_table_entries.find_or_create_by!(:ancestor_id => ancestor.id, :descendant_id => current.id, :distance => index)
362
367
  end
363
368
 
364
369
  # Now check each child to see if it is a descendant, or if we need to recurse
@@ -14,11 +14,6 @@ describe 'acts_as_dag' do
14
14
  @child = @klass.create(:name => 'child')
15
15
  end
16
16
 
17
- it "should be a root node immediately after saving" do
18
- @grandpa.parents.should be_empty
19
- @grandpa.root?.should be_true
20
- end
21
-
22
17
  it "should be descendant of itself immediately after saving" do
23
18
  @grandpa.descendants.should == [@grandpa]
24
19
  end
@@ -72,6 +67,20 @@ describe 'acts_as_dag' do
72
67
  @dad.descendants.should == [@dad,@child]
73
68
  @dad.children.should == [@child]
74
69
  end
70
+
71
+ it "should return ancestors in order of greatest distance to least" do
72
+ @dad.add_child(@child)
73
+ @grandpa.add_child(@dad)
74
+
75
+ @child.ancestors.should == [@grandpa, @dad, @child]
76
+ end
77
+
78
+ it "should return descendants in order of of least distance to greatest" do
79
+ @dad.add_child(@child)
80
+ @grandpa.add_child(@dad)
81
+
82
+ @grandpa.descendants.should == [@grandpa, @dad, @child]
83
+ end
75
84
 
76
85
  it "should be able to test descent" do
77
86
  @dad.add_child(@child)
@@ -81,7 +90,18 @@ describe 'acts_as_dag' do
81
90
  @child.descendant_of?(@grandpa).should be_true
82
91
  @child.ancestor_of?(@grandpa).should be_false
83
92
  @grandpa.descendant_of?(@child).should be_false
84
- end
93
+ end
94
+
95
+ it "should be a root node immediately after saving" do
96
+ @grandpa.parents.should be_empty
97
+ @grandpa.root?.should be_true
98
+ end
99
+
100
+ it "should be a child if it has a parent" do
101
+ @grandpa.add_child(@dad)
102
+ @grandpa.add_child(@mom)
103
+ @klass.children.order(:id).should == [@dad, @mom]
104
+ end
85
105
  end
86
106
 
87
107
  describe "reorganization" do
@@ -151,7 +171,7 @@ describe 'acts_as_dag' do
151
171
  @big_totem_pole_model.reload.children.should == [@big_red_model_totem_pole]
152
172
  end
153
173
 
154
- describe "when there is a single long inheritance chain" do
174
+ describe "when there is a single long inheritance chain with multiple paths between an ancestor and descendant" do
155
175
  before(:each) do
156
176
  @totem.add_child(@totem_pole)
157
177
  @totem_pole.add_child(@big_totem_pole)
@@ -159,6 +179,14 @@ describe 'acts_as_dag' do
159
179
  @big_model_totem_pole.add_child(@big_red_model_totem_pole)
160
180
  end
161
181
 
182
+ it "should not return multiple instances of any descendants" do
183
+ @totem.descendants.should == [@totem, @totem_pole, @big_totem_pole, @big_model_totem_pole, @big_red_model_totem_pole]
184
+ end
185
+
186
+ it "should not return multiple instances of any ancestors" do
187
+ @big_red_model_totem_pole.ancestors.should == [@totem, @totem_pole, @big_totem_pole, @big_model_totem_pole, @big_red_model_totem_pole]
188
+ end
189
+
162
190
  describe "and we are reorganizing the middle of the chain" do
163
191
  # Totem
164
192
  # |
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: acts_as_dag
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.2
4
+ version: 1.2.0
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -11,7 +11,23 @@ autorequire:
11
11
  bindir: bin
12
12
  cert_chain: []
13
13
  date: 2013-01-24 00:00:00.000000000 Z
14
- dependencies: []
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: activerecord
17
+ requirement: !ruby/object:Gem::Requirement
18
+ none: false
19
+ requirements:
20
+ - - ~>
21
+ - !ruby/object:Gem::Version
22
+ version: '4.0'
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ none: false
27
+ requirements:
28
+ - - ~>
29
+ - !ruby/object:Gem::Version
30
+ version: '4.0'
15
31
  description:
16
32
  email: technical@rrnpilot.org
17
33
  executables: []
@@ -33,18 +49,18 @@ require_paths:
33
49
  required_ruby_version: !ruby/object:Gem::Requirement
34
50
  none: false
35
51
  requirements:
36
- - - ! '>='
52
+ - - '>='
37
53
  - !ruby/object:Gem::Version
38
54
  version: '0'
39
55
  required_rubygems_version: !ruby/object:Gem::Requirement
40
56
  none: false
41
57
  requirements:
42
- - - ! '>='
58
+ - - '>='
43
59
  - !ruby/object:Gem::Version
44
60
  version: '0'
45
61
  requirements: []
46
62
  rubyforge_project:
47
- rubygems_version: 1.8.10
63
+ rubygems_version: 1.8.25
48
64
  signing_key:
49
65
  specification_version: 3
50
66
  summary: Adds directed acyclic graph functionality to ActiveRecord.