acts_as_sane_tree 1.0

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