acts_as_sane_tree 1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/README.rdoc ADDED
@@ -0,0 +1,39 @@
1
+ == acts_as_sane_tree
2
+ ==== (Building trees with a dash of sanity)
3
+
4
+ This is a drop in replacement for acts_as_tree on systems with Postgresql >= 8.4
5
+
6
+ == What this provides
7
+
8
+ A fast way to build trees.
9
+
10
+ == Requirements
11
+
12
+ * PostgreSQL version >= 8.4
13
+ * ActiveRecord
14
+
15
+ == Configuration
16
+
17
+ Same as acts_as_tree. Basically: Specify a parent_id or the column that holds the parent ID information.
18
+
19
+ == Extras
20
+
21
+ A few extras are provided. Of note are:
22
+
23
+ * #depth -> depth from root of the current node
24
+ * #descendents -> all descendents of the current node (provided in either flat array or nested hash)
25
+ * nodes_within? -> true if any of the nodes provided in the first parameter are found within the selfs or descendents of the second parameter
26
+ * nodes_within -> same as #nodes_within? but returns array of found nodes
27
+
28
+ == Thanks
29
+
30
+ * Thanks to David Hansson for the original
31
+ * Thanks to PostgreSQL for providing tools for sanity
32
+
33
+ == License
34
+
35
+ A large majority of this was copied directly from the original acts as tree, with the original license:
36
+ * Copyright (c) 2007 David Heinemeier Hansson, released under the MIT license
37
+
38
+ The new additions continue:
39
+ * Copyright (c) 2010 Chris Roberts, released under the MIT license
@@ -0,0 +1,22 @@
1
+ $LOAD_PATH.unshift File.expand_path(File.dirname(__FILE__)) + '/lib/'
2
+ require 'lib/version'
3
+ Gem::Specification.new do |s|
4
+ s.name = 'acts_as_sane_tree'
5
+ s.version = ActsAsSaneTree::VERSION
6
+ s.summary = 'Sane tree builder for ActiveRecord and Postgresql'
7
+ s.author = 'Chris Roberts'
8
+ s.email = 'chrisroberts.code@gmail.com'
9
+ s.homepage = 'http://github.com/chrisroberts/acts_as_sane_tree'
10
+ s.description = 'Sane ActiveRecord tree builder'
11
+ s.require_path = 'lib'
12
+ s.has_rdoc = true
13
+ s.extra_rdoc_files = ['README.rdoc']
14
+ s.add_dependency 'activerecord'
15
+ s.files = %w{
16
+ acts_as_sane_tree.gemspec
17
+ init.rb
18
+ README.rdoc
19
+ lib/acts_as_sane_tree.rb
20
+ lib/version.rb
21
+ }
22
+ end
data/init.rb ADDED
@@ -0,0 +1 @@
1
+ require 'acts_as_sane_tree'
@@ -0,0 +1,202 @@
1
+ module ActsAsSaneTree
2
+ def self.included(base)
3
+ base.extend(ClassMethods)
4
+ end
5
+
6
+ # Specify this +acts_as+ extension if you want to model a tree structure by providing a parent association and a children
7
+ # association. This requires that you have a foreign key column, which by default is called +parent_id+.
8
+ #
9
+ # class Category < ActiveRecord::Base
10
+ # acts_as_sane_tree :order => "name"
11
+ # end
12
+ #
13
+ # Example:
14
+ # root
15
+ # \_ child1
16
+ # \_ subchild1
17
+ # \_ subchild2
18
+ #
19
+ # root = Category.create("name" => "root")
20
+ # child1 = root.children.create("name" => "child1")
21
+ # subchild1 = child1.children.create("name" => "subchild1")
22
+ #
23
+ # root.parent # => nil
24
+ # child1.parent # => root
25
+ # root.children # => [child1]
26
+ # root.children.first.children.first # => subchild1
27
+ #
28
+ # In addition to the parent and children associations, the following instance methods are added to the class
29
+ # after calling <tt>acts_as_sane_tree</tt>:
30
+ # * <tt>siblings</tt> - Returns all the children of the parent, excluding the current node (<tt>[subchild2]</tt> when called on <tt>subchild1</tt>)
31
+ # * <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>)
32
+ # * <tt>ancestors</tt> - Returns all the ancestors of the current node (<tt>[child1, root]</tt> when called on <tt>subchild2</tt>)
33
+ # * <tt>root</tt> - Returns the root of the current node (<tt>root</tt> when called on <tt>subchild2</tt>)
34
+ # * <tt>nodes_within?(src, chk)</tt> - Returns true if any nodes provided in chk are found within the nodes in src or the descendents of the nodes in chk
35
+ module ClassMethods
36
+ # Configuration options are:
37
+ #
38
+ # * <tt>foreign_key</tt> - specifies the column name to use for tracking of the tree (default: +parent_id+)
39
+ # * <tt>order</tt> - makes it possible to sort the children according to this SQL snippet.
40
+ # * <tt>counter_cache</tt> - keeps a count in a +children_count+ column if set to +true+ (default: +false+).
41
+ def acts_as_sane_tree(options = {})
42
+ configuration = { :foreign_key => "parent_id", :order => nil, :counter_cache => nil }
43
+ configuration.update(options) if options.is_a?(Hash)
44
+
45
+ belongs_to :parent, :class_name => name, :foreign_key => configuration[:foreign_key], :counter_cache => configuration[:counter_cache]
46
+ has_many :children, :class_name => name, :foreign_key => configuration[:foreign_key], :order => configuration[:order], :dependent => :destroy
47
+
48
+ class_eval <<-EOV
49
+ include ActsAsSaneTree::InstanceMethods
50
+
51
+ def self.roots
52
+ find(:all, :conditions => "#{configuration[:foreign_key]} IS NULL", :order => #{configuration[:order].nil? ? "nil" : %Q{"#{configuration[:order]}"}})
53
+ end
54
+
55
+ def self.root
56
+ find(:first, :conditions => "#{configuration[:foreign_key]} IS NULL", :order => #{configuration[:order].nil? ? "nil" : %Q{"#{configuration[:order]}"}})
57
+ end
58
+
59
+ def self.nodes_within?(src, chk)
60
+ s = (src.is_a?(Array) ? src : [src]).map{|x|x.is_a?(ActiveRecord::Base) ? x.id : x.to_i}
61
+ c = (chk.is_a?(Array) ? chk : [chk]).map{|x|x.is_a?(ActiveRecord::Base) ? x.id : x.to_i}
62
+ if(s.empty? || c.empty?)
63
+ false
64
+ else
65
+ q = self.connection.select_all(
66
+ "WITH RECURSIVE crumbs AS (
67
+ SELECT #{self.table_name}.*, 0 AS level FROM #{self.table_name} WHERE id in (\#{s.join(', ')})
68
+ UNION ALL
69
+ SELECT alias1.*, crumbs.level + 1 FROM crumbs JOIN #{self.table_name} alias1 on alias1.parent_id = crumbs.id
70
+ ) SELECT count(*) as count FROM crumbs WHERE id in (\#{c.join(', ')})"
71
+ )
72
+ q.first['count'].to_i > 0
73
+ end
74
+ end
75
+
76
+ def self.nodes_within(src, chk)
77
+ s = (src.is_a?(Array) ? src : [src]).map{|x|x.is_a?(ActiveRecord::Base) ? x.id : x.to_i}
78
+ c = (chk.is_a?(Array) ? chk : [chk]).map{|x|x.is_a?(ActiveRecord::Base) ? x.id : x.to_i}
79
+ if(s.empty? || c.empty?)
80
+ nil
81
+ else
82
+ self.find_by_sql(
83
+ "WITH RECURSIVE crumbs AS (
84
+ SELECT #{self.table_name}.*, 0 AS level FROM #{self.table_name} WHERE id in (\#{s.join(', ')})
85
+ UNION ALL
86
+ SELECT alias1.*, crumbs.level + 1 FROM crumbs JOIN #{self.table_name} alias1 on alias1.parent_id = crumbs.id
87
+ ) SELECT * FROM crumbs WHERE id in (\#{c.join(', ')})"
88
+ )
89
+ end
90
+ end
91
+
92
+ EOV
93
+ end
94
+ end
95
+
96
+ module InstanceMethods
97
+
98
+ # Returns all ancestors of the current node.
99
+ def ancestors
100
+ self.class.find_by_sql "WITH RECURSIVE crumbs AS (
101
+ SELECT #{self.class.table_name}.*,
102
+ 1 AS level
103
+ FROM #{self.class.table_name}
104
+ WHERE id = #{id}
105
+ UNION ALL
106
+ SELECT alias1.*,
107
+ level + 1
108
+ FROM crumbs
109
+ JOIN #{self.class.table_name} alias1 ON alias1.id = crumbs.parent_id
110
+ ) SELECT * FROM crumbs ORDER BY level DESC"
111
+ end
112
+
113
+ # Returns the root node of the tree.
114
+ def root
115
+ ancestors.first
116
+ end
117
+
118
+ # Returns all siblings of the current node.
119
+ #
120
+ # subchild1.siblings # => [subchild2]
121
+ def siblings
122
+ self_and_siblings - [self]
123
+ end
124
+
125
+ # Returns all siblings and a reference to the current node.
126
+ #
127
+ # subchild1.self_and_siblings # => [subchild1, subchild2]
128
+ def self_and_siblings
129
+ parent ? parent.children : self.class.roots
130
+ end
131
+
132
+ # Returns if the current node is a root
133
+ def root?
134
+ parent_id.nil?
135
+ end
136
+
137
+ # Returns all descendents of the current node. Each level
138
+ # is within its own hash, so for a structure like:
139
+ # root
140
+ # \_ child1
141
+ # \_ subchild1
142
+ # \_ subsubchild1
143
+ # \_ subchild2
144
+ # the resulting hash would look like:
145
+ #
146
+ # -> {child1 =>
147
+ # {subchild1 =>
148
+ # {subsubchild1 => {}},
149
+ # subchild2 => {}}}
150
+ #
151
+ # This method will accept two parameters.
152
+ # * :raw -> Result is flat array. No Hash tree is built
153
+ # * {:depth => n} -> Will only search for descendents to the given depth of n
154
+ def descendents(*args)
155
+ depth = args.detect{|x|x.is_a?(Hash) && x[:depth]}
156
+ depth = depth[:depth] if depth
157
+ raw = args.include?(:raw)
158
+ q = self.class.find_by_sql(
159
+ "WITH RECURSIVE crumbs AS (
160
+ SELECT #{self.class.table_name}.*, -1 AS level FROM #{self.class.table_name} WHERE id = #{id}
161
+ UNION ALL
162
+ SELECT alias1.*, crumbs.level + 1 FROM crumbs JOIN #{self.class.table_name} alias1 on alias1.parent_id = crumbs.id
163
+ #{depth ? "WHERE crumbs.level + 1 < #{depth.to_i}" : ''}
164
+ ) SELECT * FROM crumbs WHERE level >= 0 ORDER BY level, parent_id ASC"
165
+ )
166
+ unless(raw)
167
+ res = {}
168
+ cache = {}
169
+ q.each do |item|
170
+ cache[item.id] = {}
171
+ if(cache[item.parent_id])
172
+ cache[item.parent_id][item] = cache[item.id]
173
+ else
174
+ res[item] = cache[item.id]
175
+ end
176
+ end
177
+ res
178
+ else
179
+ q
180
+ end
181
+ end
182
+
183
+ # Returns the depth of the current node. 0 depth represents the root
184
+ # of the tree
185
+ def depth
186
+ res = self.class.connection.select_all(
187
+ "WITH RECURSIVE crumbs AS (
188
+ SELECT parent_id, 0 AS level
189
+ FROM #{self.class.table_name}
190
+ WHERE id = #{id}
191
+ UNION ALL
192
+ SELECT alias1.parent_id, level + 1
193
+ FROM crumbs
194
+ JOIN #{self.class.table_name} alias1 ON alias1.id = crumbs.parent_id
195
+ ) SELECT level FROM crumbs ORDER BY level DESC LIMIT 1"
196
+ )
197
+ res.empty? ? nil : res.first['level']
198
+ end
199
+ end
200
+ end
201
+
202
+ ActiveRecord::Base.send :include, ActsAsSaneTree
data/lib/version.rb ADDED
@@ -0,0 +1,3 @@
1
+ module ActsAsSaneTree
2
+ VERSION = '1.0'
3
+ end
metadata ADDED
@@ -0,0 +1,83 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: acts_as_sane_tree
3
+ version: !ruby/object:Gem::Version
4
+ hash: 15
5
+ prerelease: false
6
+ segments:
7
+ - 1
8
+ - 0
9
+ version: "1.0"
10
+ platform: ruby
11
+ authors:
12
+ - Chris Roberts
13
+ autorequire:
14
+ bindir: bin
15
+ cert_chain: []
16
+
17
+ date: 2010-10-25 00:00:00 -07:00
18
+ default_executable:
19
+ dependencies:
20
+ - !ruby/object:Gem::Dependency
21
+ name: activerecord
22
+ prerelease: false
23
+ requirement: &id001 !ruby/object:Gem::Requirement
24
+ none: false
25
+ requirements:
26
+ - - ">="
27
+ - !ruby/object:Gem::Version
28
+ hash: 3
29
+ segments:
30
+ - 0
31
+ version: "0"
32
+ type: :runtime
33
+ version_requirements: *id001
34
+ description: Sane ActiveRecord tree builder
35
+ email: chrisroberts.code@gmail.com
36
+ executables: []
37
+
38
+ extensions: []
39
+
40
+ extra_rdoc_files:
41
+ - README.rdoc
42
+ files:
43
+ - acts_as_sane_tree.gemspec
44
+ - init.rb
45
+ - README.rdoc
46
+ - lib/acts_as_sane_tree.rb
47
+ - lib/version.rb
48
+ has_rdoc: true
49
+ homepage: http://github.com/chrisroberts/acts_as_sane_tree
50
+ licenses: []
51
+
52
+ post_install_message:
53
+ rdoc_options: []
54
+
55
+ require_paths:
56
+ - lib
57
+ required_ruby_version: !ruby/object:Gem::Requirement
58
+ none: false
59
+ requirements:
60
+ - - ">="
61
+ - !ruby/object:Gem::Version
62
+ hash: 3
63
+ segments:
64
+ - 0
65
+ version: "0"
66
+ required_rubygems_version: !ruby/object:Gem::Requirement
67
+ none: false
68
+ requirements:
69
+ - - ">="
70
+ - !ruby/object:Gem::Version
71
+ hash: 3
72
+ segments:
73
+ - 0
74
+ version: "0"
75
+ requirements: []
76
+
77
+ rubyforge_project:
78
+ rubygems_version: 1.3.7
79
+ signing_key:
80
+ specification_version: 3
81
+ summary: Sane tree builder for ActiveRecord and Postgresql
82
+ test_files: []
83
+