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.
- data/lib/acts_as_dag/acts_as_dag.rb +34 -29
- data/spec/acts_as_dag_spec.rb +35 -7
- metadata +21 -5
@@ -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,
|
20
|
-
belongs_to :child,
|
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,
|
26
|
-
belongs_to :descendant, :class_name => '#{self.name}', :foreign_key =>
|
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:
|
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 :
|
60
|
-
has_many :
|
61
|
-
|
62
|
-
has_many :
|
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 :
|
65
|
-
has_many :
|
66
|
-
|
67
|
-
|
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}
|
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.
|
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 =>
|
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.
|
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
|
data/spec/acts_as_dag_spec.rb
CHANGED
@@ -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.
|
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.
|
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.
|