acts_as_happy_tree 1.0.0 → 1.0.1

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.
@@ -5,7 +5,7 @@
5
5
 
6
6
  Gem::Specification.new do |s|
7
7
  s.name = %q{acts_as_happy_tree}
8
- s.version = "1.0.0"
8
+ s.version = "1.0.1"
9
9
 
10
10
  s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
11
  s.authors = ["David Heinemeier Hansson", "Jim Gay", "thoughtafter", "and others"]
@@ -22,11 +22,15 @@ Gem::Specification.new do |s|
22
22
  "acts_as_happy_tree.gemspec",
23
23
  "init.rb",
24
24
  "rails/init.rb",
25
- "test/acts_as_happy_tree_test.rb"
25
+ "test/acts_as_happy_tree_test.rb",
26
+ "lib/active_record/acts/happy_tree.rb",
27
+ "lib/active_record/acts/breadth_first.rb",
28
+ "lib/active_record/acts/depth_first.rb",
29
+ "lib/acts_as_happy_tree.rb",
26
30
  ]
27
31
  s.homepage = %q{http://github.com/thoughtafter/acts_as_happy_tree}
28
32
  s.require_paths = ["lib"]
29
- s.rubygems_version = %q{1.0.0}
33
+ s.rubygems_version = %q{1.0.1}
30
34
  s.summary = %q{acts_as_happy_tree gem}
31
35
 
32
36
  if s.respond_to? :specification_version then
@@ -0,0 +1,93 @@
1
+ module ActiveRecord
2
+ module Acts
3
+ module HappyTree
4
+ module BreadthFirst
5
+
6
+ def each_level_ids(options={})
7
+ level_ids = [id]
8
+ until level_ids.empty?
9
+ level_ids = self.class.child_ids_of(level_ids, options)
10
+ yield level_ids
11
+ end
12
+ end
13
+
14
+ def each_level_nodes(options={})
15
+ level_nodes = [self]
16
+ until level_nodes.empty?
17
+ ids = level_nodes.map(&:id)
18
+ level_nodes = self.class.children_of(ids, options)
19
+ yield level_nodes
20
+ end
21
+ end
22
+
23
+ # Returns a flat list of the descendants of the current node using a
24
+ # breadth-first search http://en.wikipedia.org/wiki/Breadth-first_search
25
+ #
26
+ # root.descendants_bfs # => [child1, child2, subchild1, subchild2]
27
+ # options can be passed such as:
28
+ # select - only return specified attributes, must include id
29
+ # conditions - only return matching objects, will not return children
30
+ # of any unmatched objects
31
+ # order - will set the order of each set of children
32
+ # limit - will limit max number of children for each parent
33
+ #
34
+ # number of DB calls == number of levels
35
+ # for the example there will be 3 DB calls
36
+ def descendants_bfs(options={})
37
+ desc_nodes = []
38
+ each_level_nodes(options) do |nodes|
39
+ desc_nodes += nodes
40
+ end
41
+ return desc_nodes
42
+ end
43
+
44
+ # Returns a flat list of the descendant ids of the current node using a
45
+ # breadth-first search http://en.wikipedia.org/wiki/Breadth-first_search
46
+ # DB calls = level of tree
47
+ # only id field returned in query
48
+ def descendant_ids_bfs(options={})
49
+ desc_ids = []
50
+ each_level_ids(options) do |level_ids|
51
+ desc_ids += level_ids
52
+ end
53
+ return desc_ids
54
+ end
55
+
56
+ # Return the number of descendants
57
+ # DB calls = level of tree
58
+ # only id field selected in query
59
+ def descendants_count_bfs(options={})
60
+ count = 0
61
+ each_level_ids(options) do |level_ids|
62
+ count += level_ids.count
63
+ end
64
+ return count
65
+ end
66
+
67
+ # Return all descendants and current node, this is present for
68
+ # completeness with other tree implementations
69
+ def self_and_descendants_bfs(options={})
70
+ [self] + descendants_bfs(options)
71
+ end
72
+
73
+ def descendant_ids_bfs_rec(options={})
74
+ descendants_bfs_rec(options.merge(:select=>'id')).map(&:id)
75
+ end
76
+
77
+ def descendants_bfs_rec(options={})
78
+ c = children.all(options)
79
+ c + c.map do |child|
80
+ child.descendants_bfs_rec(options)
81
+ end.flatten
82
+ end
83
+
84
+ def descendants_count_bfs_rec(options={})
85
+ children.count + children.all(options.merge(:select=>'id')).reduce(0) do |count, child|
86
+ count += child.descendants_count_bfs_rec(options)
87
+ end
88
+ end
89
+
90
+ end
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,93 @@
1
+ module ActiveRecord
2
+ module Acts
3
+ module HappyTree
4
+ module DepthFirst
5
+
6
+ def each_descendant_id(options={})
7
+ node_ids = self.class.child_ids_of(id, options)
8
+ until node_ids.empty?
9
+ node_id = node_ids.shift
10
+ yield node_id
11
+ node_ids.unshift(*self.class.child_ids_of(node_id, options))
12
+ end
13
+ end
14
+
15
+ def each_descendant_node(options={})
16
+ nodes = self.class.children_of(id, options)
17
+ until nodes.empty?
18
+ node = nodes.shift
19
+ yield node
20
+ nodes.unshift(*self.class.children_of(node.id, options))
21
+ end
22
+ end
23
+
24
+ # Returns a flat list of the descendants of the current node using a
25
+ # depth-first search http://en.wikipedia.org/wiki/Depth-first_search
26
+ #
27
+ # root.descendants_dfs # => [child1, subchild1, subchild2, child2]
28
+ # options can be passed such as:
29
+ # select - only return specified attributes, must include "parent_id"
30
+ # conditions - only return matching objects, will not return children
31
+ # of any unmatched objects
32
+ # order - will set the order of each set of children
33
+ # limit - will limit max number of children for each parent
34
+ #
35
+ # this is a recursive method
36
+ # the number of DB calls == number of descendants + 1
37
+ def descendants_dfs(options={})
38
+ desc_nodes = []
39
+ each_descendant_node(options) do |node|
40
+ desc_nodes << node
41
+ end
42
+ return desc_nodes
43
+ end
44
+
45
+ # Returns a flat list of the descendant ids of the current node using a
46
+ # depth-first search http://en.wikipedia.org/wiki/Depth-first_search
47
+ # DB calls = number of descendants + 1
48
+ # only id field returned in query
49
+ def descendant_ids_dfs(options={})
50
+ desc_ids = []
51
+ each_descendant_id(options) do |id|
52
+ desc_ids << id
53
+ end
54
+ return desc_ids
55
+ end
56
+
57
+ # Return the number of descendants
58
+ # DB calls = # of descendants + 1
59
+ # only id field selected in query
60
+ def descendants_count_dfs(options={})
61
+ count = 0
62
+ each_descendant_id(options) do |id|
63
+ count += 1
64
+ end
65
+ return count
66
+ end
67
+
68
+ # Return all descendants and current node, this is present for
69
+ # completeness with other tree implementations
70
+ def self_and_descendants_dfs(options={})
71
+ [self] + descendants_dfs(options)
72
+ end
73
+
74
+ def descendant_ids_dfs_rec(options={})
75
+ descendants_dfs_rec(options.merge(:select=>'id')).map(&:id)
76
+ end
77
+
78
+ def descendants_dfs_rec(options={})
79
+ children.all(options).map do |child|
80
+ [child] + child.descendants_dfs_rec(options)
81
+ end.flatten
82
+ end
83
+
84
+ def descendants_count_dfs_rec(options={})
85
+ children.all(options.merge(:select=>'id')).reduce(0) do |count, child|
86
+ count += 1 + child.descendants_count_dfs_rec(options)
87
+ end
88
+ end
89
+
90
+ end
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,381 @@
1
+ module ActiveRecord
2
+ module Acts
3
+ module HappyTree
4
+ def self.included(base)
5
+ base.extend(ClassMethods)
6
+ end
7
+
8
+ # Specify this +acts_as+ extension if you want to model a tree structure by providing a parent association and a children
9
+ # association. This requires that you have a foreign key column, which by default is called +parent_id+.
10
+ #
11
+ # class Category < ActiveRecord::Base
12
+ # acts_as_happy_tree :order => "name"
13
+ # end
14
+ #
15
+ # Example:
16
+ # root
17
+ # \_ child1
18
+ # \_ subchild1
19
+ # \_ subchild2
20
+ #
21
+ # root = Category.create("name" => "root")
22
+ # child1 = root.children.create("name" => "child1")
23
+ # subchild1 = child1.children.create("name" => "subchild1")
24
+ #
25
+ # root.parent # => nil
26
+ # child1.parent # => root
27
+ # root.children # => [child1]
28
+ # root.children.first.children.first # => subchild1
29
+ #
30
+ # In addition to the parent and children associations, the following instance methods are added to the class
31
+ # after calling <tt>acts_as_happy_tree</tt>:
32
+ # * <tt>siblings</tt> - Returns all the children of the parent, excluding the current node (<tt>[subchild2]</tt> when called on <tt>subchild1</tt>)
33
+ # * <tt>self_and_siblings</tt> - Returns all the children of the parent, including the current node (<tt>[subchild1, subchild2]</tt> when called on <tt>subchild1</tt>)
34
+ # * <tt>ancestors</tt> - Returns all the ancestors of the current node (<tt>[child1, root]</tt> when called on <tt>subchild2</tt>)
35
+ # * <tt>root</tt> - Returns the root of the current node (<tt>root</tt> when called on <tt>subchild2</tt>)
36
+ # * <tt>descendants</tt> - Returns a flat list of the descendants of the current node (<tt>[child1, subchild1, subchild2]</tt> when called on <tt>root</tt>)
37
+ module ClassMethods
38
+ # Configuration options are:
39
+ #
40
+ # * <tt>foreign_key</tt> - specifies the column name to use for tracking of the tree (default: +parent_id+)
41
+ # * <tt>order</tt> - makes it possible to sort the children according to this SQL snippet.
42
+ # * <tt>counter_cache</tt> - keeps a count in a +children_count+ column if set to +true+ (default: +false+).
43
+ def acts_as_happy_tree(options = {})
44
+ configuration = { :foreign_key => "parent_id", :order => nil, :counter_cache => nil, :dependent => :destroy, :touch => false }
45
+ configuration.update(options) if options.is_a?(Hash)
46
+
47
+ belongs_to :parent, :class_name => name, :foreign_key => configuration[:foreign_key], :counter_cache => configuration[:counter_cache], :touch => configuration[:touch]
48
+ has_many :children, :class_name => name, :foreign_key => configuration[:foreign_key], :order => configuration[:order], :dependent => configuration[:dependent]
49
+
50
+ validate :parent_key_must_be_valid
51
+
52
+ class_eval <<-EOV
53
+ include ActiveRecord::Acts::HappyTree::InstanceMethods
54
+ include ActiveRecord::Acts::HappyTree::BreadthFirst
55
+ include ActiveRecord::Acts::HappyTree::DepthFirst
56
+
57
+ scope :roots, :conditions => "#{configuration[:foreign_key]} IS NULL", :order => #{configuration[:order].nil? ? "nil" : %Q{"#{configuration[:order]}"}}
58
+
59
+ after_update :update_parents_counter_cache
60
+
61
+ def self.root
62
+ roots.first
63
+ end
64
+
65
+ def self.childless
66
+ nodes = []
67
+
68
+ find(:all).each do |node|
69
+ nodes << node if node.children.empty?
70
+ end
71
+
72
+ nodes
73
+ end
74
+
75
+ def self.parent_key
76
+ "#{configuration[:foreign_key]}"
77
+ end
78
+ EOV
79
+ end
80
+
81
+ # AR >= 3.2 only
82
+ def pluck_parent_id_of(node_id)
83
+ where(:id=>node_id).pluck(parent_key).first
84
+ end
85
+
86
+ def pluck_child_ids_of(node_ids, options={})
87
+ where(parent_key=>node_ids).apply_finder_options(options).pluck(:id)
88
+ end
89
+
90
+ # AR >= 3.0 only
91
+ def select_parent_id_of(node_id)
92
+ where(:id=>node_id).select(parent_key).first[parent_key]
93
+ end
94
+
95
+ def select_child_ids_of(node_ids, options={})
96
+ where(parent_key=>node_ids).apply_finder_options(options.merge(:select=>:id)).map(&:id)
97
+ end
98
+
99
+ if ActiveRecord::Base.respond_to?(:pluck)
100
+ alias :parent_id_of :pluck_parent_id_of
101
+ alias :child_ids_of :pluck_child_ids_of
102
+ else
103
+ alias :parent_id_of :select_parent_id_of
104
+ alias :child_ids_of :select_child_ids_of
105
+ end
106
+
107
+ def children_of(node_ids, options={})
108
+ where(parent_key=>node_ids).apply_finder_options(options)
109
+ end
110
+
111
+ end
112
+
113
+ module InstanceMethods
114
+ # Returns list of ancestors, starting from parent until root.
115
+ #
116
+ # subchild1.ancestors # => [child1, root]
117
+ def ancestors
118
+ node, nodes = self, []
119
+ nodes << node = node.parent until node.parent.nil? and return nodes
120
+ end
121
+
122
+ # Returns the root node of the tree.
123
+ def root_classic
124
+ node = self
125
+ node = node.parent until node.parent.nil? and return node
126
+ end
127
+
128
+ # Returns all siblings of the current node.
129
+ #
130
+ # subchild1.siblings # => [subchild2]
131
+ def siblings
132
+ self_and_siblings - [self]
133
+ end
134
+
135
+ # Returns all siblings and a reference to the current node.
136
+ #
137
+ # subchild1.self_and_siblings # => [subchild1, subchild2]
138
+ def self_and_siblings
139
+ parent ? parent.children : self.class.roots
140
+ end
141
+
142
+ # Returns a flat list of the descendants of the current node.
143
+ #
144
+ # root.descendants # => [child1, subchild1, subchild2]
145
+ def descendants_classic(node=self)
146
+ nodes = []
147
+ nodes << node unless node == self
148
+
149
+ node.children.each do |child|
150
+ nodes += descendants_classic(child)
151
+ end
152
+
153
+ nodes.compact
154
+ end
155
+
156
+ def childless
157
+ self.descendants.collect{|d| d.children.empty? ? d : nil}.compact
158
+ end
159
+
160
+ # Returns true if this instance has no parent (aka root node)
161
+ #
162
+ # root.root? # => true
163
+ # child1.root? # => false
164
+ #
165
+ # no DB access
166
+ def root?
167
+ tree_parent_key.nil?
168
+ end
169
+
170
+ # Returns true if this instance has a parent (aka child node)
171
+ #
172
+ # root.child? # => false
173
+ # child1.child? # => true
174
+ #
175
+ # no DB access
176
+ def child?
177
+ !tree_parent_key.nil?
178
+ end
179
+
180
+ # returns true if this instance has any children (aka parent node)
181
+ #
182
+ # root.parent? # => true
183
+ # subchild1.parent? # => false
184
+ #
185
+ # 1 DB SELECT, no fields selected
186
+ def parent?
187
+ children.exists?
188
+ end
189
+
190
+ # returns true if this instance has no children (aka leaf node)
191
+ #
192
+ # root.leaf? # => false
193
+ # subchild1.leaf? # => true
194
+ #
195
+ # 1 DB SELECT, no fields selected
196
+ def leaf?
197
+ !children.exists?
198
+ end
199
+
200
+ # Returns true if this instance is an ancestor of another instance
201
+ #
202
+ # root.ancestor_of?(child1) # => true
203
+ # child1.ancestor_of?(root) # => false
204
+ #
205
+ # 1 DB SELECT per level examined, only selects "parent_id"
206
+ # AR < 3.2 = 1 AR object per level examined
207
+ # AR >= 3.2 = no AR objects
208
+ #
209
+ # equivalent of node.descendant_of?(self)
210
+ # node1.ancestor_of(node2) == node2.descendant_of(node1)
211
+ def ancestor_of?(node)
212
+ return false if (node.nil? || !node.is_a?(self.class))
213
+ key = node.tree_parent_key
214
+ until key.nil? do
215
+ return true if key == self.id
216
+ key = self.class.parent_id_of(key)
217
+ end
218
+ return false
219
+ end
220
+
221
+ # Returns true if this instance is a descendant of another instance
222
+ #
223
+ # root.descendant_of?(child1) # => false
224
+ # child1.descendant_of?(root) # => true
225
+ #
226
+ # same performance as ancestor_of?
227
+ #
228
+ # equivalent of node.ancestor_of?(self)
229
+ # node1.descendant_of(node2) == node2.ancestor_of(node1)
230
+ def descendant_of?(node)
231
+ return false if (node.nil? || !node.is_a?(self.class))
232
+ key = self.tree_parent_key
233
+ until key.nil? do
234
+ return true if key == node.id
235
+ key = self.class.parent_id_of(key)
236
+ end
237
+ return false
238
+ end
239
+
240
+ # Returns list of ancestor ids, starting from parent until root.
241
+ #
242
+ # subchild1.ancestor_ids # => [child1.id, root.id]
243
+ #
244
+ # 1 DB SELECT per ancestor, only selects "parent_id"
245
+ # AR < 3.2 = 1 AR object per ancestor
246
+ # AR >= 3.2 = 0 AR objects
247
+ def ancestor_ids
248
+ key, node_ids = tree_parent_key, []
249
+ until key.nil? do
250
+ node_ids << key
251
+ key = self.class.parent_id_of(key)
252
+ end
253
+ return node_ids
254
+ end
255
+
256
+ # Returns a count of the number of ancestors
257
+ #
258
+ # subchild1.ancestors_count # => 2
259
+ #
260
+ # 1 DB SELECT per ancestor, only selects "parent_id"
261
+ # AR < 3.2 = 1 AR object per ancestor
262
+ # AR >= 3.2 = 0 AR objects
263
+ def ancestors_count
264
+ key, count = tree_parent_key, 0
265
+ until key.nil? do
266
+ count += 1
267
+ key = self.class.parent_id_of(key)
268
+ end
269
+ return count
270
+ end
271
+
272
+ # Returns the root id of the current node
273
+ # no DB access if node is root
274
+ # otherwise use root_id function which is optimized
275
+ # 1 DB SELECT per ancestor, only selects "parent_id"
276
+ # AR < 3.2 = 1 AR object per ancestor
277
+ # AR >= 3.2 = 0 AR objects
278
+ def root_id
279
+ key, node_id = tree_parent_key, id
280
+ until key.nil? do
281
+ node_id = key
282
+ key = self.class.parent_id_of(key)
283
+ end
284
+ return node_id
285
+ end
286
+
287
+ # Returns the root node of the current node
288
+ # no DB access if node is root
289
+ # otherwise use root_id function which is optimized
290
+ # performance = root_id + 1 DB SELECT and 1 AR object if not root
291
+ # other acts_as_tree variants select all fields and instantiate all objects
292
+ def root
293
+ root? ? self : self.class.find(root_id)
294
+ end
295
+
296
+ # helper method to allow the choosing of the descendants traversal
297
+ # method by setting :traversal option as follows:
298
+ #
299
+ # :classic - depth-first search, recursive
300
+ # - only for descendants, ignores finder options
301
+ # :dfs - depth-first search, iterative
302
+ # :dfs_rec - depth-first search, recursive
303
+ # :bfs - breadth-first search, interative
304
+ # :bfs_rec - breadth-first search, recursive
305
+ def descendants_call(method, default, options={})
306
+ traversal = options.delete(:traversal)
307
+ case traversal
308
+ when :classic
309
+ send("#{method}_classic")
310
+ when :bfs, :dfs, :bfs_rec, :dfs_rec
311
+ send("#{method}_#{traversal}", options)
312
+ else
313
+ send("#{method}_#{default}", options)
314
+ end
315
+ end
316
+
317
+ # returns all of the descendants
318
+ # uses iterative method in DFS order by default
319
+ # options are finder options
320
+ def descendants(options={})
321
+ descendants_call(:descendants, :dfs, options)
322
+ end
323
+
324
+ # return self and descendants
325
+ # provided for compatibility with other tree implementations
326
+ # uses iterative method in DFS order by default
327
+ # options are finder options
328
+ def self_and_descendants(options={})
329
+ descendants_call(:self_and_descendants, :dfs, options)
330
+ end
331
+
332
+ # return an array of descendant ids
333
+ # uses iterative method in DFS order by default
334
+ # options are finder options
335
+ def descendant_ids(options={})
336
+ descendants_call(:descendant_ids, :dfs, options)
337
+ end
338
+
339
+ # return a count of the number of descendants
340
+ # Use BFS for descendants_count because it should use fewer SQL queries
341
+ # and thus be faster
342
+ # options are finder options
343
+ def descendants_count(options={})
344
+ descendants_call(:descendants_count, :bfs, options)
345
+ end
346
+
347
+ # method for validating parent_key to make sure it is not
348
+ # 1) the same as id
349
+ # 2) already a descendant of the current node
350
+ def parent_key_must_be_valid
351
+ return if id.nil?
352
+ if (tree_parent_key==id)
353
+ errors.add(tree_parent_key_name, "#{tree_parent_key_name} cannot be the same as id")
354
+ elsif ancestor_of?(parent)
355
+ errors.add(tree_parent_key_name, "#{tree_parent_key_name} cannot be a descendant")
356
+ end
357
+ end
358
+
359
+ private
360
+
361
+ def update_parents_counter_cache
362
+ if self.respond_to?(:children_count) && parent_id_changed?
363
+ self.class.decrement_counter(:children_count, parent_id_was)
364
+ self.class.increment_counter(:children_count, parent_id)
365
+ end
366
+ end
367
+
368
+ protected
369
+
370
+ def tree_parent_key_name
371
+ self.class.parent_key
372
+ end
373
+
374
+ def tree_parent_key
375
+ attributes[tree_parent_key_name]
376
+ end
377
+
378
+ end
379
+ end
380
+ end
381
+ end
@@ -0,0 +1,4 @@
1
+ require 'active_record/acts/breadth_first'
2
+ require 'active_record/acts/depth_first'
3
+ require 'active_record/acts/happy_tree'
4
+ ActiveRecord::Base.send :include, ActiveRecord::Acts::HappyTree
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: acts_as_happy_tree
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0
4
+ version: 1.0.1
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -29,6 +29,10 @@ files:
29
29
  - init.rb
30
30
  - rails/init.rb
31
31
  - test/acts_as_happy_tree_test.rb
32
+ - lib/active_record/acts/happy_tree.rb
33
+ - lib/active_record/acts/breadth_first.rb
34
+ - lib/active_record/acts/depth_first.rb
35
+ - lib/acts_as_happy_tree.rb
32
36
  homepage: http://github.com/thoughtafter/acts_as_happy_tree
33
37
  licenses: []
34
38
  post_install_message: