acts_as_dag 1.2.6 → 2.0.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.
@@ -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