acts_as_happy_tree 1.0.0 → 1.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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: