acts_as_dag 1.1.2 → 1.2.0

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