acts_as_dag 1.2.6 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,121 @@
1
+ module ActsAsDAG
2
+ module Deprecated
3
+
4
+ module ClassMethods
5
+ # Reorganizes the entire class of records based on their name, first resetting the hierarchy, then reoganizing
6
+ # Can pass a list of categories and only those will be reorganized
7
+ def reorganize(categories_to_reorganize = self.all)
8
+ puts "This method is deprecated and will be removed in a future version"
9
+
10
+ return if categories_to_reorganize.empty?
11
+
12
+ reset_hierarchy(categories_to_reorganize)
13
+
14
+ word_count_groups = categories_to_reorganize.group_by{|category| ActsAsDAG::Deprecated::HelperMethods.word_count(category)}.sort
15
+ roots_categories = word_count_groups.first[1].dup.sort_by(&:name) # We will build up a list of plinko targets, we start with the group of categories with the shortest word count
16
+
17
+ # Now plinko the next shortest word group into those targets
18
+ # If we can't plinko one, then it gets added as a root
19
+ word_count_groups[1..-1].each do |word_count, categories|
20
+ categories_with_no_parents = []
21
+
22
+ # Try drop each category into each root
23
+ categories.sort_by(&:name).each do |category|
24
+ ActiveRecord::Base.benchmark "Analyze #{category.name}" do
25
+ suitable_parent = false
26
+ roots_categories.each do |root|
27
+ suitable_parent = true if ActsAsDAG::Deprecated::HelperMethods.plinko(root, category)
28
+ end
29
+ unless suitable_parent
30
+ ActiveRecord::Base.logger.info { "Plinko couldn't find a suitable parent for #{category.name}" }
31
+ categories_with_no_parents << category
32
+ end
33
+ end
34
+ end
35
+
36
+ # Add all categories from this group without suitable parents to the roots
37
+ if categories_with_no_parents.present?
38
+ ActiveRecord::Base.logger.info { "Adding #{categories_with_no_parents.collect(&:name).join(', ')} to roots" }
39
+ roots_categories.concat categories_with_no_parents
40
+ end
41
+ end
42
+ end
43
+ end
44
+
45
+ module InstanceMethods
46
+ end
47
+
48
+ module HelperMethods
49
+ # Searches the subtree for the best parent for the other
50
+ # 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
51
+ def self.plinko(current, other)
52
+ # ActiveRecord::Base.logger.info { "Plinkoing '#{other.name}' into '#{current.name}'..." }
53
+ if should_descend_from?(current, other)
54
+ # Find the subtree of the current category that +other+ should descend from
55
+ subtree_other_should_descend_from = current.subtree.select{|record| should_descend_from?(record, other) }
56
+ # Of those, find the categories with the most number of matching words and make +other+ their child
57
+ # We find all suitable candidates to provide support for categories whose names are permutations of each other
58
+ # e.g. 'goat wool fibre' should be a child of 'goat wool' and 'wool goat' if both are present under 'goat'
59
+ new_parents_group = subtree_other_should_descend_from.group_by{|category| matching_word_count(other, category)}.sort.reverse.first
60
+ if new_parents_group.present?
61
+ for new_parent in new_parents_group[1]
62
+ ActiveRecord::Base.logger.info { " '#{other.name}' landed under '#{new_parent.name}'" }
63
+ other.add_parent(new_parent)
64
+
65
+ # We've just affected the associations in ways we can not possibly imagine, so let's clear the association cache
66
+ current.clear_association_cache
67
+ end
68
+ return true
69
+ end
70
+ end
71
+ end
72
+
73
+ # Convenience method for plinkoing multiple categories
74
+ # Plinko's multiple categories from shortest to longest in order to prevent the need for reorganization
75
+ def self.plinko_multiple(current, others)
76
+ groups = others.group_by{|category| word_count(category)}.sort
77
+ groups.each do |word_count, categories|
78
+ categories.each do |category|
79
+ unless plinko(current, category)
80
+ end
81
+ end
82
+ end
83
+ end
84
+
85
+ # Returns the portion of this category's name that is not present in any of it's parents
86
+ def self.unique_name_portion(current)
87
+ unique_portion = current.name.split
88
+ for parent in current.parents
89
+ for word in parent.name.split
90
+ unique_portion.delete(word)
91
+ end
92
+ end
93
+
94
+ return unique_portion.empty? ? nil : unique_portion.join(' ')
95
+ end
96
+
97
+ # Checks if other should descend from +current+ based on name matching
98
+ # Returns true if other contains all the words from +current+, but has words that are not contained in +current+
99
+ def self.should_descend_from?(current, other)
100
+ return false if current == other
101
+
102
+ other_words = other.name.split
103
+ current_words = current.name.split
104
+
105
+ # (other contains all the words from current and more) && (current contains no words that are not also in other)
106
+ return (other_words - (current_words & other_words)).count > 0 && (current_words - other_words).count == 0
107
+ end
108
+
109
+ def self.word_count(current)
110
+ current.name.split.count
111
+ end
112
+
113
+ def self.matching_word_count(current, other)
114
+ other_words = other.name.split
115
+ self_words = current.name.split
116
+ return (other_words & self_words).count
117
+ end
118
+
119
+ end
120
+ end
121
+ end
@@ -1,314 +1,838 @@
1
1
  require 'spec_helper'
2
2
 
3
3
  describe 'acts_as_dag' do
4
+ before do
5
+ klass.destroy_all # Because we're using sqlite3 and it doesn't support transactional specs (afaik)
6
+ end
7
+
4
8
  shared_examples_for "DAG Model" do
5
- before(:each) do
6
- @klass.destroy_all # Because we're using sqlite3 and it doesn't support transactional specs (afaik)
9
+ let (:grandpa) { klass.create(:name => 'grandpa') }
10
+ let (:dad) { klass.create(:name => 'dad') }
11
+ let (:mom) { klass.create(:name => 'mom') }
12
+ let (:suzy) { klass.create(:name => 'suzy') }
13
+ let (:billy) { klass.create(:name => 'billy') }
14
+
15
+ describe '#children' do
16
+ it "returns an ActiveRecord::Relation" do
17
+ expect(mom.children).to be_an(ActiveRecord::Relation)
18
+ end
19
+
20
+ it "includes all children of the receiver" do
21
+ mom.add_child(suzy, billy)
22
+ expect(mom.children).to include(suzy,billy)
23
+ end
24
+
25
+ it "doesn't include any records that are not children of the receiver" do
26
+ grandpa.add_child(mom)
27
+ expect(mom.children).not_to include(grandpa)
28
+ end
29
+
30
+ it "returns records in the order they were added to the graph" do
31
+ grandpa.add_child(mom, dad)
32
+ expect(grandpa.children).to eq([mom, dad])
33
+ end
7
34
  end
8
35
 
9
- describe "and" do
10
- before(:each) do
11
- @grandpa = @klass.create(:name => 'grandpa')
12
- @dad = @klass.create(:name => 'dad')
13
- @mom = @klass.create(:name => 'mom')
14
- @child = @klass.create(:name => 'child')
36
+ describe '#parents' do
37
+ it "returns an ActiveRecord::Relation" do
38
+ expect(mom.parents).to be_an(ActiveRecord::Relation)
15
39
  end
16
40
 
17
- it "should be descendant of itself immediately after saving" do
18
- @grandpa.descendants.should == [@grandpa]
41
+ it "includes all parents of the receiver" do
42
+ suzy.add_parent(mom, dad)
43
+ expect(suzy.parents).to include(mom, dad)
19
44
  end
20
45
 
21
- it "should be ancestor of itself immediately after saving" do
22
- @grandpa.ancestors.should == [@grandpa]
46
+ it "doesn't include any records that are not parents of the receiver" do
47
+ dad.add_parent(grandpa)
48
+ suzy.add_parent(mom, dad)
49
+ expect(suzy.parents).not_to include(grandpa)
23
50
  end
24
51
 
25
- it "should be able to add a child" do
26
- @grandpa.add_child(@dad)
52
+ it "returns records in the order they were added to the graph" do
53
+ suzy.add_parent(mom, dad)
54
+ expect(suzy.parents).to eq([mom, dad])
55
+ end
56
+ end
27
57
 
28
- @grandpa.children.should == [@dad]
58
+ describe '#descendants' do
59
+ it "returns an ActiveRecord::Relation" do
60
+ expect(mom.descendants).to be_an(ActiveRecord::Relation)
29
61
  end
30
62
 
31
- it "should be able to add a parent" do
32
- @child.add_parent(@dad)
63
+ it "doesn't include self" do
64
+ expect(mom.descendants).not_to include(mom)
65
+ end
33
66
 
34
- @child.parents.should == [@dad]
67
+ it "includes all descendants of the receiver" do
68
+ grandpa.add_child(mom, dad)
69
+ mom.add_child(suzy)
70
+ dad.add_child(billy)
71
+ expect(grandpa.descendants).to include(mom, dad, suzy, billy)
35
72
  end
36
73
 
37
- it "should be able to add multiple parents" do
38
- @child.add_parent(@dad)
39
- @child.add_parent(@mom)
74
+ it "doesn't include any ancestors of the receiver" do
75
+ grandpa.add_child(mom)
76
+ mom.add_child(suzy)
77
+ expect(mom.descendants).not_to include(grandpa)
78
+ end
40
79
 
41
- @child.parents.sort_by(&:id).should == [@dad, @mom].sort_by(&:id)
80
+ it "returns records in ascending order of distance, and ascending order added to graph" do
81
+ grandpa.add_child(mom, dad)
82
+ mom.add_child(suzy)
83
+ dad.add_child(billy)
84
+ expect(grandpa.descendants).to eq([mom, dad, suzy, billy])
42
85
  end
43
86
 
44
- it "should be able to add multiple children" do
45
- @grandpa.add_child(@dad)
46
- @grandpa.add_child(@mom)
87
+ it "returns no duplicates when there are multiple paths to the same descendant" do
88
+ grandpa.add_child(mom, dad)
89
+ billy.add_parent(mom, dad)
90
+
91
+ expect(grandpa.descendants).to eq(grandpa.descendants.uniq)
92
+ end
93
+ end
94
+
95
+ describe '#subtree' do
96
+ it "returns an ActiveRecord::Relation" do
97
+ expect(mom.subtree).to be_an(ActiveRecord::Relation)
98
+ end
99
+
100
+ it "includes self" do
101
+ expect(mom.subtree).to include(mom)
102
+ end
103
+
104
+ it "includes all descendants of the receiver" do
105
+ grandpa.add_child(mom)
106
+ mom.add_child(billy)
107
+ expect(grandpa.subtree).to include(mom, billy)
108
+ end
109
+
110
+ it "doesn't include any ancestors of the receiver" do
111
+ grandpa.add_child(mom)
112
+ mom.add_child(billy)
113
+ expect(mom.subtree).not_to include(grandpa)
114
+ end
115
+
116
+ it "returns records in ascending order of distance, and ascending order added to graph" do
117
+ grandpa.add_child(mom)
118
+ grandpa.add_child(dad)
119
+ mom.add_child(billy)
120
+ expect(grandpa.subtree).to eq([grandpa, mom, dad, billy])
121
+ end
122
+
123
+ it "returns no duplicates when there are multiple paths to the same descendant" do
124
+ grandpa.add_child(mom, dad)
125
+ billy.add_parent(mom, dad)
126
+
127
+ expect(grandpa.subtree).to eq(grandpa.subtree.uniq)
128
+ end
129
+ end
47
130
 
48
- @grandpa.children.sort_by(&:id).should == [@dad, @mom].sort_by(&:id)
131
+ describe '#ancestors' do
132
+ it "returns an ActiveRecord::Relation" do
133
+ expect(mom.ancestors).to be_an(ActiveRecord::Relation)
49
134
  end
50
135
 
51
- it "should be able to add ancestors (top down)" do
52
- @grandpa.add_child(@dad)
53
- @dad.add_child(@child)
136
+ it "doesn't include self" do
137
+ expect(mom.ancestors).not_to include(mom)
138
+ end
54
139
 
55
- @grandpa.children.should == [@dad]
56
- @grandpa.descendants.sort_by(&:id).should == [@grandpa, @dad, @child].sort_by(&:id)
57
- @dad.descendants.should == [@dad, @child]
58
- @dad.children.should == [@child]
140
+ it "includes all ancestors of the receiver" do
141
+ grandpa.add_child(mom)
142
+ mom.add_child(billy)
143
+ expect(billy.ancestors).to include(grandpa, mom)
59
144
  end
60
145
 
61
- it "should be able to add ancestors (bottom up)" do
62
- @dad.add_child(@child)
63
- @grandpa.add_child(@dad)
146
+ it "doesn't include any descendants of the receiver" do
147
+ grandpa.add_child(mom)
148
+ mom.add_child(billy)
149
+ expect(mom.ancestors).not_to include(billy)
150
+ end
64
151
 
65
- @grandpa.children.should == [@dad]
66
- @grandpa.descendants.sort_by(&:id).should == [@grandpa, @dad, @child].sort_by(&:id)
67
- @dad.descendants.should == [@dad,@child]
68
- @dad.children.should == [@child]
152
+ it "returns records in descending order of distance, and ascending order added to graph" do
153
+ grandpa.add_child(mom)
154
+ mom.add_child(billy)
155
+ dad.add_child(billy)
156
+ expect(billy.ancestors).to eq([grandpa, mom, dad])
69
157
  end
70
158
 
71
- it "should return ancestors in order of greatest distance to least" do
72
- @dad.add_child(@child)
73
- @grandpa.add_child(@dad)
159
+ it "returns no duplicates when there are multiple paths to the same ancestor" do
160
+ grandpa.add_child(mom, dad)
161
+ billy.add_parent(mom, dad)
74
162
 
75
- @child.ancestors.should == [@grandpa, @dad, @child]
163
+ expect(billy.ancestors).to eq(billy.ancestors.uniq)
76
164
  end
165
+ end
77
166
 
78
- it "should return descendants in order of of least distance to greatest" do
79
- @dad.add_child(@child)
80
- @grandpa.add_child(@dad)
167
+ describe '#path' do
168
+ it "returns an ActiveRecord::Relation" do
169
+ expect(mom.path).to be_an(ActiveRecord::Relation)
170
+ end
81
171
 
82
- @grandpa.descendants.should == [@grandpa, @dad, @child]
172
+ it "includes self" do
173
+ expect(mom.path).to include(mom)
83
174
  end
84
175
 
85
- it "should be able to test descent" do
86
- @dad.add_child(@child)
87
- @grandpa.add_child(@dad)
176
+ it "includes all ancestors of the receiver" do
177
+ grandpa.add_child(mom)
178
+ mom.add_child(billy)
179
+ dad.add_child(billy)
180
+ expect(billy.path).to include(grandpa, mom, dad)
181
+ end
88
182
 
89
- @grandpa.ancestor_of?(@child).should be_truthy
90
- @child.descendant_of?(@grandpa).should be_truthy
91
- @child.ancestor_of?(@grandpa).should be_falsy
92
- @grandpa.descendant_of?(@child).should be_falsy
183
+ it "doesn't include any descendants of the receiver" do
184
+ grandpa.add_child(mom)
185
+ mom.add_child(billy)
186
+ expect(mom.path).not_to include(billy)
93
187
  end
94
188
 
95
- it "should be a root node immediately after saving" do
96
- @grandpa.parents.should be_empty
97
- @grandpa.root?.should be_truthy
189
+ it "returns records in descending order of distance, and ascending order added to graph" do
190
+ grandpa.add_child(mom)
191
+ mom.add_child(billy)
192
+ dad.add_child(billy)
193
+ expect(billy.path).to eq([grandpa, mom, dad, billy])
98
194
  end
99
195
 
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]
196
+ it "returns no duplicates when there are multiple paths to the same ancestor" do
197
+ grandpa.add_child(mom, dad)
198
+ billy.add_parent(mom, dad)
199
+
200
+ expect(billy.path).to eq(billy.path.uniq)
104
201
  end
105
202
  end
106
203
 
107
- context "when a record hierarchy exists" do
108
- before(:each) do
109
- @grandma = @klass.create(:name => 'grandma')
110
- @mom = @klass.create(:name => 'mom')
111
- @brother = @klass.create(:name => 'brother')
204
+ describe '#add_child' do
205
+ it "makes the record a child of the receiver" do
206
+ mom.add_child(billy)
207
+ expect(billy.child_of?(mom)).to be_truthy
208
+ end
112
209
 
113
- @grandma.add_child(@mom)
114
- @mom.add_child(@brother)
210
+ it "makes the record a descendant of the receiver" do
211
+ mom.add_child(billy)
212
+ expect(billy.descendant_of?(mom)).to be_truthy
115
213
  end
116
214
 
117
- it "destroying a record should delete the associated hierarchy-tracking records " do
118
- @mom.destroy
119
- @mom.descendant_links.should be_empty
120
- @mom.ancestor_links.should be_empty
121
- @mom.parent_links.should be_empty
122
- @mom.child_links.should be_empty
215
+ it "makes the record an descendant of any of the receiver's ancestors" do
216
+ grandpa.add_child(mom)
217
+ mom.add_child(billy)
218
+ expect(billy.descendant_of?(grandpa)).to be_truthy
123
219
  end
124
220
 
125
- it "make_root should make the record a root, but maintain it's children" do
126
- @mom.make_root
221
+ it "can be called multiple times to add additional children" do
222
+ mom.add_child(suzy)
223
+ mom.add_child(billy)
224
+ expect(mom.children).to include(suzy, billy)
225
+ end
127
226
 
128
- @mom.should be_root
129
- @mom.parents.should be_empty
130
- @mom.children.should be_present
227
+ it "accepts multiple arguments, adding each as a child" do
228
+ mom.add_child(suzy, billy)
229
+ expect(mom.children).to include(suzy, billy)
131
230
  end
132
231
 
133
- it "should return a list of ancestors and descendants" do
134
- @mom.relatives.should == (@mom.ancestors + @mom.descendants).uniq
232
+ it "accepts an array of records, adding each as a child" do
233
+ mom.add_child([suzy, billy])
234
+ expect(mom.children).to include(suzy, billy)
135
235
  end
136
236
  end
137
237
 
138
- describe "reorganization" do
139
- before(:each) do
140
- @totem = @klass.create(:name => "totem")
141
- @totem_pole = @klass.create(:name => "totem pole")
142
- @big_totem_pole = @klass.create(:name => "big totem pole")
143
- @big_model_totem_pole = @klass.create(:name => "big model totem pole")
144
- @big_red_model_totem_pole = @klass.create(:name => "big red model totem pole")
238
+ describe '#add_parent' do
239
+ it "makes the record a parent of the receiver" do
240
+ suzy.add_parent(dad)
241
+ expect(dad.parent_of?(suzy)).to be_truthy
145
242
  end
146
243
 
147
- it "should reinitialize links and descendants after resetting the hierarchy" do
148
- @klass.reset_hierarchy
149
- @big_totem_pole.parents.should == []
150
- @big_totem_pole.children.should == []
151
- @big_totem_pole.ancestors.should == [@big_totem_pole]
152
- @big_totem_pole.descendants.should == [@big_totem_pole]
244
+ it "makes the record a ancestor of the receiver" do
245
+ suzy.add_parent(dad)
246
+ expect(dad.ancestor_of?(suzy)).to be_truthy
153
247
  end
154
248
 
155
- it "should be able to determine whether one category is an ancestor of the other by inspecting the name" do
156
- ActsAsDAG::HelperMethods.should_descend_from?(@totem_pole, @big_totem_pole).should be_truthy
157
- ActsAsDAG::HelperMethods.should_descend_from?(@big_totem_pole, @totem_pole).should be_falsy
249
+ it "makes the record an ancestor of any of the receiver's ancestors" do
250
+ dad.add_parent(grandpa)
251
+ suzy.add_parent(dad)
252
+ expect(grandpa.ancestor_of?(suzy)).to be_truthy
158
253
  end
159
254
 
160
- it "should be able to determine the number of matching words in two categories names" do
161
- ActsAsDAG::HelperMethods.matching_word_count(@totem_pole, @big_totem_pole).should == 2
255
+ it "can be called multiple times to add additional parents" do
256
+ suzy.add_parent(mom)
257
+ suzy.add_parent(dad)
258
+ expect(suzy.parents).to include(mom, dad)
162
259
  end
163
260
 
164
- it "should arrange the categories correctly when not passed any arguments" do
165
- @klass.reorganize
261
+ it "accepts multiple arguments, adding each as a parent" do
262
+ suzy.add_parent(mom, dad)
263
+ expect(suzy.parents).to include(mom, dad)
264
+ end
166
265
 
167
- @totem.children.should == [@totem_pole]
168
- @totem_pole.children.should == [@big_totem_pole]
169
- @big_totem_pole.children.should == [@big_model_totem_pole]
170
- @big_model_totem_pole.children.should == [@big_red_model_totem_pole]
266
+ it "accepts an array of records, adding each as a parent" do
267
+ suzy.add_parent([mom, dad])
268
+ expect(suzy.parents).to include(mom, dad)
171
269
  end
270
+ end
172
271
 
173
- it "should arrange the categories correctly when passed a set of nodes to reorganize" do
174
- @klass.reorganize [@totem, @totem_pole, @big_totem_pole, @big_model_totem_pole, @big_red_model_totem_pole]
272
+ describe '#ancestor_of?' do
273
+ it "returns true if the record is a ancestor of the receiver" do
274
+ grandpa.add_child(mom)
275
+ mom.add_child(billy)
276
+ expect(grandpa.ancestor_of?(billy)).to be_truthy
277
+ end
175
278
 
176
- @totem.reload.children.should == [@totem_pole]
177
- @totem_pole.reload.children.should == [@big_totem_pole]
178
- @big_totem_pole.reload.children.should == [@big_model_totem_pole]
179
- @big_model_totem_pole.reload.children.should == [@big_red_model_totem_pole]
279
+ it "returns false if the record is not an ancestor of the receiver" do
280
+ grandpa.add_child(dad)
281
+ mom.add_child(billy)
282
+ expect(grandpa.ancestor_of?(billy)).to be_falsey
180
283
  end
284
+ end
181
285
 
182
- it "should arrange the categories correctly when inserting a category into an existing chain" do
183
- @totem.add_child(@big_totem_pole)
286
+ describe '#descendant_of?' do
287
+ it "returns true if the record is a descendant of the receiver" do
288
+ grandpa.add_child(mom)
289
+ mom.add_child(billy)
290
+ expect(billy.descendant_of?(grandpa)).to be_truthy
291
+ end
184
292
 
185
- @klass.reorganize
293
+ it "returns false if the record is not an descendant of the receiver" do
294
+ grandpa.add_child(dad)
295
+ mom.add_child(billy)
296
+ expect(billy.descendant_of?(grandpa)).to be_falsey
297
+ end
298
+ end
186
299
 
187
- @totem.children.should == [@totem_pole]
188
- @totem_pole.children.should == [@big_totem_pole]
189
- @big_totem_pole.children.should == [@big_model_totem_pole]
190
- @big_model_totem_pole.reload.children.should == [@big_red_model_totem_pole]
300
+ describe '#child_of?' do
301
+ it "returns true if the record is a child of the receiver" do
302
+ mom.add_child(billy)
303
+ expect(billy.child_of?(mom)).to be_truthy
191
304
  end
192
305
 
193
- it "should still work when there are categories that are permutations of each other" do
194
- @big_totem_pole_model = @klass.create(:name => "big totem pole model")
306
+ it "returns false if the record is not an child of the receiver" do
307
+ mom.add_child(suzy)
308
+ expect(billy.child_of?(mom)).to be_falsey
309
+ end
310
+ end
195
311
 
196
- @klass.reorganize [@totem, @totem_pole, @big_totem_pole, @big_model_totem_pole, @big_red_model_totem_pole, @big_totem_pole_model]
312
+ describe '#parent_of?' do
313
+ it "returns true if the record is a parent of the receiver" do
314
+ mom.add_child(billy)
315
+ expect(mom.parent_of?(billy)).to be_truthy
316
+ end
197
317
 
198
- @totem.children.should == [@totem_pole]
199
- @totem_pole.children.should == [@big_totem_pole]
200
- (@big_totem_pole.children - [@big_model_totem_pole, @big_totem_pole_model]).should == []
201
- @big_model_totem_pole.reload.children.should == [@big_red_model_totem_pole]
202
- @big_totem_pole_model.reload.children.should == [@big_red_model_totem_pole]
318
+ it "returns false if the record is not an parent of the receiver" do
319
+ mom.add_child(billy)
320
+ expect(mom.parent_of?(suzy)).to be_falsey
203
321
  end
322
+ end
204
323
 
205
- describe "when there is a single long inheritance chain" do
206
- before(:each) do
207
- @totem.add_child(@totem_pole)
208
- @totem_pole.add_child(@big_totem_pole)
209
- @big_totem_pole.add_child(@big_model_totem_pole)
210
- @big_model_totem_pole.add_child(@big_red_model_totem_pole)
211
- end
324
+ describe '#root?' do
325
+ it "returns true if the record has no parents" do
326
+ mom.add_child(suzy)
327
+ expect(mom.root?).to be_truthy
328
+ end
212
329
 
213
- describe "and we are reorganizing the middle of the chain" do
214
- # Totem
215
- # |
216
- # Totem Pole
217
- # *|* \
218
- # *|* Big Totem Pole
219
- # *|* /
220
- # Big Model Totem Pole
221
- # |
222
- # Big Red Model Totem Pole
223
- #
224
- before(:each) do
225
- @totem_pole.add_child(@big_model_totem_pole)
226
- end
227
-
228
- it "should return multiple instances of descendants before breaking the old link" do
229
- @totem.descendants.sort_by(&:id).should == [@totem, @totem_pole, @big_totem_pole, @big_model_totem_pole, @big_model_totem_pole, @big_red_model_totem_pole, @big_red_model_totem_pole].sort_by(&:id)
230
- end
231
-
232
- it "should return the correct inheritance chain after breaking the old link" do
233
- @totem_pole.remove_child(@big_model_totem_pole)
234
-
235
- @totem_pole.children.sort_by(&:id).should == [@big_totem_pole].sort_by(&:id)
236
- @totem.descendants.sort_by(&:id).should == [@totem, @totem_pole, @big_totem_pole, @big_model_totem_pole, @big_red_model_totem_pole].sort_by(&:id)
237
- end
238
-
239
- it "should return the correct inheritance chain after breaking the old link when there is are two ancestor root nodes" do
240
- pole = @klass.create(:name => "pole")
241
- @totem_pole.add_parent(pole)
242
- @totem_pole.remove_child(@big_model_totem_pole)
243
-
244
- pole.descendants.sort_by(&:id).should == [pole, @totem_pole, @big_totem_pole, @big_model_totem_pole, @big_red_model_totem_pole].sort_by(&:id)
245
- @totem_pole.children.sort_by(&:id).should == [@big_totem_pole].sort_by(&:id)
246
- @totem.descendants.sort_by(&:id).should == [@totem, @totem_pole, @big_totem_pole, @big_model_totem_pole, @big_red_model_totem_pole].sort_by(&:id)
247
- end
248
- end
330
+ it "returns false if the record has parents" do
331
+ mom.add_parent(grandpa)
332
+ expect(mom.root?).to be_falsey
249
333
  end
250
334
  end
251
335
 
252
- describe "and two paths of the same length exist to the same node" do
253
- before(:each) do
254
- @grandpa = @klass.create(:name => 'grandpa')
255
- @dad = @klass.create(:name => 'dad')
256
- @mom = @klass.create(:name => 'mom')
257
- @child = @klass.create(:name => 'child')
336
+ describe '#leaf?' do
337
+ it "returns true if the record has no children" do
338
+ mom.add_parent(grandpa)
339
+ expect(mom.leaf?).to be_truthy
340
+ end
258
341
 
259
- # nevermind the incest
260
- @grandpa.add_child(@dad)
261
- @dad.add_child(@child)
262
- @child.add_parent(@mom)
263
- @mom.add_parent(@grandpa)
342
+ it "returns false if the record has children" do
343
+ mom.add_child(suzy)
344
+ expect(mom.leaf?).to be_falsey
264
345
  end
346
+ end
265
347
 
266
- it "descendants should not return multiple instances of a child" do
267
- @grandpa.descendants.sort_by(&:id).should == [@grandpa, @dad, @mom, @child].sort_by(&:id)
348
+ describe '#make_root' do
349
+ it "makes the receiver a root node" do
350
+ mom.add_parent(grandpa)
351
+ mom.make_root
352
+ expect(mom.root?).to be_truthy
268
353
  end
269
354
 
270
- describe "and a link between parent and ancestor is removed" do
271
- before(:each) do
272
- # the incest is undone!
273
- @dad.remove_parent(@grandpa)
274
- end
355
+ it "removes the receiver from the children of its parents" do
356
+ suzy.add_parent(mom, dad)
357
+ suzy.make_root
358
+ expect(mom.children).not_to include(suzy)
359
+ expect(dad.children).not_to include(suzy)
360
+ end
275
361
 
276
- it "should still return the correct ancestors" do
277
- @child.ancestors.sort_by(&:id).should == [@grandpa, @dad, @mom, @child].sort_by(&:id)
278
- @mom.ancestors.sort_by(&:id).should == [@grandpa, @mom].sort_by(&:id)
279
- @dad.ancestors.sort_by(&:id).should == [@dad].sort_by(&:id)
362
+ it "doesn't modify the relationship between the receiver and its descendants" do
363
+ mom.add_parent(grandpa)
364
+ mom.add_child(suzy, billy)
365
+ mom.make_root
366
+ expect(mom.children).to eq([suzy, billy])
367
+ end
368
+ end
369
+
370
+ describe '#lineage' do
371
+ it "returns an ActiveRecord::Relation" do
372
+ expect(mom.children).to be_an(ActiveRecord::Relation)
373
+ end
374
+
375
+ it "doesn't include the receiver" do
376
+ expect(mom.lineage).not_to include(mom)
377
+ end
378
+
379
+ it "includes all ancestors and descendants of the receiver" do
380
+ mom.add_child(suzy, billy)
381
+ mom.add_parent(grandpa)
382
+ expect(mom.lineage).to include(grandpa, suzy, billy)
383
+ end
384
+
385
+ it "return ancestors and descendants of the receiver in the order they would be if called separately" do
386
+ mom.add_child(suzy, billy)
387
+ mom.add_parent(grandpa)
388
+ expect(mom.lineage).to eq([grandpa, suzy, billy])
389
+ end
390
+ end
391
+
392
+ describe '::children' do
393
+ it "returns an ActiveRecord::Relation" do
394
+ expect(klass.children).to be_an(ActiveRecord::Relation)
395
+ end
396
+
397
+ it "returns records that have at least 1 parent" do
398
+ mom.add_parent(grandpa)
399
+ mom.add_child(suzy)
400
+ expect(klass.children).to include(mom, suzy)
401
+ end
402
+
403
+ it "doesn't returns records without parents" do
404
+ mom.add_parent(grandpa)
405
+ mom.add_child(suzy)
406
+ expect(klass.children).not_to include(grandpa)
407
+ end
408
+
409
+ it "does not return duplicate records, regardless of the number of parents" do
410
+ suzy.add_parent(mom, dad)
411
+ expect(klass.children).to eq([suzy])
412
+ end
413
+ end
414
+
415
+ describe '::parent_records' do
416
+ it "returns an ActiveRecord::Relation" do
417
+ expect(klass.parent_records).to be_an(ActiveRecord::Relation)
418
+ end
419
+
420
+ it "returns records that have at least 1 child" do
421
+ mom.add_parent(grandpa)
422
+ mom.add_child(suzy)
423
+ expect(klass.parent_records).to include(grandpa, mom)
424
+ end
425
+
426
+ it "doesn't returns records without children" do
427
+ mom.add_parent(grandpa)
428
+ mom.add_child(suzy)
429
+ expect(klass.parent_records).not_to include(suzy)
430
+ end
431
+
432
+ it "does not return duplicate records, regardless of the number of children" do
433
+ mom.add_child(suzy, billy)
434
+ expect(klass.parent_records).to eq([mom])
435
+ end
436
+ end
437
+
438
+ describe '#ancestors_of' do
439
+ it "returns an ActiveRecord::Relation" do
440
+ expect(klass.ancestors_of(suzy)).to be_an(ActiveRecord::Relation)
441
+ end
442
+
443
+ it "doesn't include the given record" do
444
+ expect(klass.ancestors_of(suzy)).not_to include(suzy)
445
+ end
446
+
447
+ it "returns records that are ancestors of the given record" do
448
+ suzy.add_parent(mom, dad)
449
+ expect(klass.ancestors_of(suzy)).to include(mom, dad)
450
+ end
451
+
452
+ it "doesn't return records that are not ancestors of the given record" do
453
+ suzy.add_parent(mom)
454
+ expect(klass.ancestors_of(suzy)).not_to include(dad)
455
+ end
456
+
457
+ it "returns records that are ancestors of the given record id" do
458
+ suzy.add_parent(mom, dad)
459
+ expect(klass.ancestors_of(suzy.id)).to include(mom, dad)
460
+ end
461
+ end
462
+
463
+ describe '#descendants_of' do
464
+ it "returns an ActiveRecord::Relation" do
465
+ expect(klass.descendants_of(grandpa)).to be_an(ActiveRecord::Relation)
466
+ end
467
+
468
+ it "doesn't include the given record" do
469
+ expect(klass.descendants_of(grandpa)).not_to include(grandpa)
470
+ end
471
+
472
+ it "returns records that are descendants of the given record" do
473
+ grandpa.add_child(mom, dad)
474
+ expect(klass.descendants_of(grandpa)).to include(mom, dad)
475
+ end
476
+
477
+ it "doesn't return records that are not descendants of the given record" do
478
+ grandpa.add_child(mom)
479
+ expect(klass.descendants_of(grandpa)).not_to include(dad)
480
+ end
481
+
482
+ it "returns records that are descendants of the given record id" do
483
+ grandpa.add_child(mom, dad)
484
+ expect(klass.descendants_of(grandpa.id)).to include(mom, dad)
485
+ end
486
+ end
487
+
488
+ describe '#path_of' do
489
+ it "returns an ActiveRecord::Relation" do
490
+ expect(klass.path_of(suzy)).to be_an(ActiveRecord::Relation)
491
+ end
492
+
493
+ it "returns records that are path-members of the given record" do
494
+ suzy.add_parent(mom, dad)
495
+ expect(klass.path_of(suzy)).to include(mom, dad, suzy)
496
+ end
497
+
498
+ it "doesn't return records that are not path-members of the given record" do
499
+ suzy.add_parent(mom)
500
+ expect(klass.path_of(suzy)).not_to include(dad)
501
+ end
502
+
503
+ it "returns records that are path-members of the given record id" do
504
+ suzy.add_parent(mom, dad)
505
+ expect(klass.path_of(suzy.id)).to include(mom, dad, suzy)
506
+ end
507
+ end
508
+
509
+ describe '#subtree_of' do
510
+ it "returns an ActiveRecord::Relation" do
511
+ expect(klass.subtree_of(grandpa)).to be_an(ActiveRecord::Relation)
512
+ end
513
+
514
+ it "returns records that are subtree-members of the given record" do
515
+ grandpa.add_child(mom, dad)
516
+ expect(klass.subtree_of(grandpa)).to include(grandpa, mom, dad)
517
+ end
518
+
519
+ it "doesn't return records that are not subtree-members of the given record" do
520
+ grandpa.add_child(mom)
521
+ expect(klass.subtree_of(grandpa)).not_to include(dad)
522
+ end
523
+
524
+ it "returns records that are subtree-members of the given record id" do
525
+ grandpa.add_child(mom, dad)
526
+ expect(klass.subtree_of(grandpa.id)).to include(grandpa, mom, dad)
527
+ end
528
+ end
529
+
530
+ describe '#destroy' do
531
+ it "destroys associated hierarchy-tracking records" do
532
+ mom.add_parent(grandpa)
533
+ mom.add_child(suzy)
534
+
535
+ mom.destroy
536
+
537
+ expect(mom.ancestor_links).to contain_exactly
538
+ expect(mom.path_links).to contain_exactly
539
+ expect(mom.parent_links).to contain_exactly
540
+ expect(mom.child_links).to contain_exactly
541
+ end
542
+ end
543
+
544
+ describe '#parents=' do
545
+ before { suzy.parents = [mom, dad] }
546
+
547
+ it "sets the receiver's parents to the given array" do
548
+ expect(suzy.parents).to eq([mom, dad])
549
+ end
550
+
551
+ it "updates the ancestors of the receiver" do
552
+ expect(suzy.ancestors).to eq([mom, dad])
553
+ end
554
+
555
+ it "unsets the receiver's parents when given an empty array" do
556
+ suzy.parents = []
557
+ expect(suzy.parents).to contain_exactly
558
+ end
559
+
560
+ it "updates the ancestors of the receivers when given an empty array" do
561
+ suzy.parents = []
562
+ expect(suzy.ancestors).to contain_exactly
563
+ end
564
+ end
565
+
566
+ describe '#children=' do
567
+ before { grandpa.children = [mom, dad] }
568
+
569
+ it "sets the receiver's children to the given array" do
570
+ expect(grandpa.children).to eq([mom, dad])
571
+ end
572
+
573
+ it "updates the descendants of the receiver" do
574
+ expect(grandpa.descendants).to eq([mom, dad])
575
+ end
576
+
577
+ it "unsets the receiver's children when given an empty array" do
578
+ grandpa.children = []
579
+ expect(grandpa.children).to contain_exactly
580
+ end
581
+
582
+ it "updates the descendants of the receivers when given an empty array" do
583
+ grandpa.children = []
584
+ expect(grandpa.descendants).to contain_exactly
585
+ end
586
+ end
587
+
588
+ describe '#parent_ids=' do
589
+ before { suzy.parent_ids = [mom.id, dad.id] }
590
+
591
+ it "sets the receiver's parents to the given array" do
592
+ expect(suzy.parents).to eq([mom, dad])
593
+ end
594
+
595
+ it "updates the ancestors of the receiver" do
596
+ expect(suzy.ancestors).to eq([mom, dad])
597
+ end
598
+
599
+ it "unsets the receiver's parents when given an empty array" do
600
+ suzy.parents = []
601
+ expect(suzy.parents).to contain_exactly
602
+ end
603
+
604
+ it "updates the ancestors of the receivers when given an empty array" do
605
+ suzy.parents = []
606
+ expect(suzy.ancestors).to contain_exactly
607
+ end
608
+ end
609
+
610
+ describe '#child_ids=' do
611
+ before { grandpa.child_ids = [mom.id, dad.id] }
612
+
613
+ it "sets the receiver's children to the given array" do
614
+ expect(grandpa.children).to eq([mom, dad])
615
+ end
616
+
617
+ it "updates the descendants of the receiver" do
618
+ expect(grandpa.descendants).to eq([mom, dad])
619
+ end
620
+
621
+ it "unsets the receiver's children when given an empty array" do
622
+ grandpa.children = []
623
+ expect(grandpa.children).to contain_exactly
624
+ end
625
+
626
+ it "updates the descendants of the receivers when given an empty array" do
627
+ grandpa.children = []
628
+ expect(grandpa.descendants).to contain_exactly
629
+ end
630
+ end
631
+
632
+ describe '#create' do
633
+ it "sets the receiver's children to the given array" do
634
+ record = klass.create!(:children => [mom, dad])
635
+ expect(record.children).to contain_exactly(mom, dad)
636
+ end
637
+
638
+ it "updates the descendants of the receiver" do
639
+ record = klass.create!(:children => [mom, dad])
640
+ record.reload
641
+ expect(record.descendants).to contain_exactly(mom, dad)
642
+ end
643
+
644
+ it "sets the receiver's parents to the given array" do
645
+ record = klass.create!(:parents => [mom, dad])
646
+ expect(record.parents).to contain_exactly(mom, dad)
647
+ end
648
+
649
+ it "updates the ancestors of the receiver" do
650
+ record = klass.create!(:parents => [mom, dad])
651
+ record.reload
652
+ expect(record.ancestors).to contain_exactly(mom, dad)
653
+ end
654
+ end
655
+
656
+ describe '::reset_hierarchy' do
657
+ it "reinitialize links and descendants after resetting the hierarchy" do
658
+ mom.add_parent(grandpa)
659
+ mom.add_child(suzy)
660
+
661
+ klass.reset_hierarchy
662
+ expect(mom.parents).to contain_exactly()
663
+ expect(mom.children).to contain_exactly()
664
+ expect(mom.path).to contain_exactly(mom)
665
+ expect(mom.subtree).to contain_exactly(mom)
666
+ end
667
+ end
668
+
669
+ describe '#ancestor_links' do
670
+ it "doesn't include a link to the receiver" do
671
+ expect(mom.ancestor_links).to contain_exactly
672
+ end
673
+ end
674
+
675
+ describe '#path_links' do
676
+ it "includes a link to the receiver" do
677
+ expect(mom.path_links.first.descendant).to eq(mom)
678
+ end
679
+ end
680
+
681
+ describe '#descendant_links' do
682
+ it "doesn't include a link to the receiver" do
683
+ expect(mom.descendant_links).to contain_exactly
684
+ end
685
+ end
686
+
687
+ describe '#subtree_links' do
688
+ it "includes a link to the receiver" do
689
+ expect(mom.subtree_links.first.descendant).to eq(mom)
690
+ end
691
+ end
692
+
693
+ describe '::roots' do
694
+ it "returns all root nodes" do
695
+ mom; dad
696
+ expect(klass.roots).to include(mom, dad)
697
+ end
698
+
699
+ it "doesn't return non-root nodes" do
700
+ mom.add_child(suzy)
701
+ expect(klass.roots).not_to include(suzy)
702
+ end
703
+
704
+ it "doesn't mark returned records as readonly" do
705
+ mom; dad
706
+ expect(klass.roots.none?(&:readonly?)).to be_truthy
707
+ end
708
+ end
709
+
710
+ describe '::leafs' do
711
+ it "returns all leaf nodes" do
712
+ mom.add_child(suzy)
713
+ expect(klass.leafs).to include(suzy)
714
+ end
715
+
716
+ it "doesn't return non-leaf nodes" do
717
+ mom.add_child(suzy)
718
+ expect(klass.leafs).not_to include(mom)
719
+ end
720
+
721
+ it "doesn't mark returned records as readonly" do
722
+ mom.add_child(suzy)
723
+ expect(klass.leafs.none?(&:readonly?)).to be_truthy
724
+ end
725
+ end
726
+
727
+ describe '::children' do
728
+ it "returns all child nodes" do
729
+ mom.add_child(suzy)
730
+ expect(klass.children).to contain_exactly(suzy)
731
+ end
732
+
733
+ it "doesn't return non-child nodes" do
734
+ mom; dad
735
+ expect(klass.children).not_to include(mom, dad)
736
+ end
737
+
738
+ it "doesn't mark returned records as readonly" do
739
+ mom.add_child(suzy)
740
+ expect(klass.children.none?(&:readonly?)).to be_truthy
741
+ end
742
+ end
743
+
744
+ describe '::parent_records' do
745
+ it "returns all parent nodes" do
746
+ mom.add_child(suzy)
747
+ expect(klass.parent_records).to contain_exactly(mom)
748
+ end
749
+
750
+ it "doesn't return non-parent nodes" do
751
+ mom; dad
752
+ expect(klass.parent_records).not_to include(mom, dad)
753
+ end
754
+
755
+ it "doesn't mark returned records as readonly" do
756
+ mom.add_child(suzy)
757
+ expect(klass.parent_records.none?(&:readonly?)).to be_truthy
758
+ end
759
+ end
760
+
761
+ context "When two paths of the same length exist to the same node and a link between parent and ancestor is removed" do
762
+ before do
763
+ grandpa.add_child(mom, dad)
764
+ suzy.add_parent(mom, dad)
765
+ end
766
+
767
+ describe '#remove_parent' do
768
+ it "updates the ancestor links correctly" do
769
+ dad.remove_parent(grandpa)
770
+ expect(suzy.ancestors).to contain_exactly(grandpa, dad, mom)
771
+ expect(mom.ancestors).to contain_exactly(grandpa)
772
+ expect(dad.ancestors).to contain_exactly()
280
773
  end
281
774
 
282
- it "should still return the correct descendants" do
283
- @child.descendants.sort_by(&:id).should == [@child].sort_by(&:id)
284
- @mom.descendants.sort_by(&:id).should == [@mom, @child].sort_by(&:id)
285
- @dad.descendants.sort_by(&:id).should == [@dad, @child].sort_by(&:id)
286
- @grandpa.descendants.sort_by(&:id).should == [@grandpa, @mom, @child].sort_by(&:id)
775
+ it "updates the descendant links correctly" do
776
+ dad.remove_parent(grandpa)
777
+ expect(suzy.descendants).to contain_exactly()
778
+ expect(mom.descendants).to contain_exactly(suzy)
779
+ expect(dad.descendants).to contain_exactly(suzy)
780
+ expect(grandpa.descendants).to contain_exactly(mom, suzy)
287
781
  end
288
782
  end
289
783
  end
784
+
785
+ # describe "Includes, Eager-Loads, and Preloads" do
786
+ # before(:each) do
787
+ # dad.add_parent(grandpa)
788
+ # billy.add_parent(dad, mom)
789
+ # end
790
+
791
+ # it "should preload path in the correct order" do
792
+ # records = klass.order("#{klass.table_name}.id asc").preload(:path)
793
+
794
+ # records[0].path.should == [grandpa] # grandpa
795
+ # records[1].path.should == [grandpa, dad] # dad
796
+ # records[2].path.should == [mom] # mom
797
+ # records[3].path.should == [grandpa, dad, mom, billy] # billy
798
+ # end
799
+
800
+ # it "should eager_load path in the correct order" do
801
+ # records = klass.order("#{klass.table_name}.id asc").eager_load(:path)
802
+
803
+ # records[0].path.should == [grandpa] # grandpa
804
+ # records[1].path.should == [grandpa, dad] # dad
805
+ # records[2].path.should == [mom] # mom
806
+ # records[3].path.should == [grandpa, dad, mom, billy] # billy
807
+ # end
808
+
809
+ # it "should include path in the correct order" do
810
+ # records = klass.order("#{klass.table_name}.id asc").includes(:path)
811
+
812
+ # records[0].path.should == [grandpa] # grandpa
813
+ # records[1].path.should == [grandpa, dad] # dad
814
+ # records[2].path.should == [mom] # mom
815
+ # records[3].path.should == [grandpa, dad, mom, billy] # billy
816
+ # end
817
+ # end
290
818
  end
291
819
 
292
820
  describe "models with separate link tables" do
293
- before(:each) do
294
- @klass = SeparateLinkModel
295
- end
821
+ let(:klass) { SeparateLinkModel }
296
822
 
297
823
  it_should_behave_like "DAG Model"
298
824
  end
299
825
 
300
826
  describe "models with unified link tables" do
301
- before(:each) do
302
- @klass = UnifiedLinkModel
303
- end
827
+ let(:klass) { UnifiedLinkModel }
304
828
 
305
829
  it_should_behave_like "DAG Model"
306
830
 
307
831
  it "should create links that include the category type" do
308
- record = @klass.create!
832
+ record = klass.create!
309
833
 
310
- record.parent_links.first.category_type.should == @klass.name
311
- record.descendant_links.first.category_type.should == @klass.name
834
+ expect(record.parent_links.first.category_type).to eq(klass.name)
835
+ expect(record.subtree_links.first.category_type).to eq(klass.name)
312
836
  end
313
837
  end
314
838
  end