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