acts_as_sane_tree 1.4 → 2.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README.rdoc +16 -4
- data/lib/acts_as_sane_tree.rb +4 -231
- data/lib/acts_as_sane_tree/version.rb +1 -1
- metadata +7 -7
data/README.rdoc
CHANGED
@@ -7,6 +7,10 @@ This is a drop in replacement for acts_as_tree on systems with Postgresql >= 8.4
|
|
7
7
|
|
8
8
|
A fast way to build trees.
|
9
9
|
|
10
|
+
== What version of Rails
|
11
|
+
|
12
|
+
As of version 2, acts_as_sane_tree now proudly offers support on Rails 2 as well as Rails 3
|
13
|
+
|
10
14
|
== Requirements
|
11
15
|
|
12
16
|
* PostgreSQL version >= 8.4
|
@@ -16,18 +20,26 @@ A fast way to build trees.
|
|
16
20
|
|
17
21
|
Same as acts_as_tree. Basically: Specify a parent_id or the column that holds the parent ID information.
|
18
22
|
|
23
|
+
class MyFancyTree < ActiveRecord::Base
|
24
|
+
acts_as_sane_tree
|
25
|
+
end
|
26
|
+
|
19
27
|
== Extras
|
20
28
|
|
21
29
|
A few extras are provided. Of note are:
|
22
30
|
|
23
31
|
* #depth -> depth from root of the current node
|
24
|
-
* #descendents -> all descendents of the current node (provided in either
|
32
|
+
* #descendents -> all descendents of the current node (provided in either scope or nested hash)
|
25
33
|
* nodes_within?(src, chk) - Returns true if chk contains any nodes found within src and all ancestors of nodes within src
|
26
34
|
* nodes_within(src, chk) - Returns any matching nodes from chk found within src and all ancestors within src
|
27
|
-
* nodes_and_descendents(*args) - Returns all nodes and descendents for given IDs or records.
|
35
|
+
* nodes_and_descendents(*args) - Returns all nodes and descendents for given IDs or records. Provided in either scope or nested hash.
|
36
|
+
* Works properly with STI
|
37
|
+
* Adds 'depth' attribute to returned model instances
|
28
38
|
|
29
39
|
== Documentation
|
30
40
|
|
41
|
+
Yes, there is documentation. Please read it and find all the fun tools at your fingertips:
|
42
|
+
|
31
43
|
http://chrisroberts.github.com/acts_as_sane_tree
|
32
44
|
|
33
45
|
== Thanks
|
@@ -37,8 +49,8 @@ http://chrisroberts.github.com/acts_as_sane_tree
|
|
37
49
|
|
38
50
|
== License
|
39
51
|
|
40
|
-
A large majority of this was copied directly from the original acts as tree, with the original license:
|
52
|
+
A (now not so large) majority of this was copied directly from the original acts as tree, with the original license:
|
41
53
|
* Copyright (c) 2007 David Heinemeier Hansson, released under the MIT license
|
42
54
|
|
43
55
|
The new additions continue:
|
44
|
-
* Copyright (c)
|
56
|
+
* Copyright (c) 2011 Chris Roberts, released under the MIT license
|
data/lib/acts_as_sane_tree.rb
CHANGED
@@ -1,233 +1,6 @@
|
|
1
|
-
require '
|
1
|
+
require 'acts_as_sane_tree/acts_as_sane_tree'
|
2
|
+
require 'acts_as_sane_tree/version'
|
2
3
|
|
3
|
-
|
4
|
-
|
5
|
-
def self.included(base) # :nodoc:
|
6
|
-
base.extend(ClassMethods)
|
7
|
-
end
|
8
|
-
|
9
|
-
# Specify this +acts_as+ extension if you want to model a tree structure by providing a parent association and a children
|
10
|
-
# association. This requires that you have a foreign key column, which by default is called +parent_id+.
|
11
|
-
#
|
12
|
-
# class Category < ActiveRecord::Base
|
13
|
-
# acts_as_sane_tree :order => "name"
|
14
|
-
# end
|
15
|
-
#
|
16
|
-
# Example:
|
17
|
-
# root
|
18
|
-
# \_ child1
|
19
|
-
# \_ subchild1
|
20
|
-
# \_ subchild2
|
21
|
-
#
|
22
|
-
# root = Category.create("name" => "root")
|
23
|
-
# child1 = root.children.create("name" => "child1")
|
24
|
-
# subchild1 = child1.children.create("name" => "subchild1")
|
25
|
-
#
|
26
|
-
# root.parent # => nil
|
27
|
-
# child1.parent # => root
|
28
|
-
# root.children # => [child1]
|
29
|
-
# root.children.first.children.first # => subchild1
|
30
|
-
#
|
31
|
-
# The following class methods are also added:
|
32
|
-
#
|
33
|
-
# * <tt>nodes_within?(src, chk)</tt> - Returns true if chk contains any nodes found within src and all ancestors of nodes within src
|
34
|
-
# * <tt>nodes_within(src, chk)</tt> - Returns any matching nodes from chk found within src and all ancestors within src
|
35
|
-
# * <tt>nodes_and_descendents(*args)</tt> - Returns all nodes and descendents for given IDs or records. Accepts multiple IDs and records. Valid options:
|
36
|
-
# * :raw - No Hash nesting
|
37
|
-
# * :no_self - Will not return given nodes in result set
|
38
|
-
# * {:depth => n} - Will set maximum depth to query
|
39
|
-
# * {:to_depth => n} - Alias for :depth
|
40
|
-
# * {:at_depth => n} - Will return times at given depth (takes precedence over :depth/:to_depth)
|
41
|
-
module ClassMethods
|
42
|
-
# Configuration options are:
|
43
|
-
#
|
44
|
-
# * <tt>foreign_key</tt> - specifies the column name to use for tracking of the tree (default: +parent_id+)
|
45
|
-
# * <tt>order</tt> - makes it possible to sort the children according to this SQL snippet.
|
46
|
-
# * <tt>counter_cache</tt> - keeps a count in a +children_count+ column if set to +true+ (default: +false+).
|
47
|
-
def acts_as_sane_tree(options = {})
|
48
|
-
configuration = { :foreign_key => "parent_id", :order => nil, :counter_cache => nil, :max_depth => 10000 }
|
49
|
-
configuration.update(options) if options.is_a?(Hash)
|
50
|
-
|
51
|
-
belongs_to :parent, :class_name => name, :foreign_key => configuration[:foreign_key], :counter_cache => configuration[:counter_cache]
|
52
|
-
has_many :children, :class_name => name, :foreign_key => configuration[:foreign_key], :order => configuration[:order], :dependent => :destroy
|
53
|
-
|
54
|
-
validates_each configuration[:foreign_key] do |record, attr, value|
|
55
|
-
record.errors.add attr, 'Cannot be own parent.' if !record.id.nil? && value.to_i == record.id.to_i
|
56
|
-
end
|
57
|
-
|
58
|
-
class_eval <<-EOV
|
59
|
-
include ActsAsSaneTree::InstanceMethods
|
60
|
-
|
61
|
-
def self.roots
|
62
|
-
find(:all, :conditions => "#{configuration[:foreign_key]} IS NULL", :order => #{configuration[:order].nil? ? "nil" : %Q{"#{configuration[:order]}"}})
|
63
|
-
end
|
64
|
-
|
65
|
-
def self.root
|
66
|
-
find(:first, :conditions => "#{configuration[:foreign_key]} IS NULL", :order => #{configuration[:order].nil? ? "nil" : %Q{"#{configuration[:order]}"}})
|
67
|
-
end
|
68
|
-
|
69
|
-
def self.nodes_within?(src, chk)
|
70
|
-
s = (src.is_a?(Array) ? src : [src]).map{|x|x.is_a?(ActiveRecord::Base) ? x.id : x.to_i}
|
71
|
-
c = (chk.is_a?(Array) ? chk : [chk]).map{|x|x.is_a?(ActiveRecord::Base) ? x.id : x.to_i}
|
72
|
-
if(s.empty? || c.empty?)
|
73
|
-
false
|
74
|
-
else
|
75
|
-
q = self.connection.select_all(
|
76
|
-
"WITH RECURSIVE crumbs AS (
|
77
|
-
SELECT #{self.table_name}.*, 0 AS level FROM #{self.table_name} WHERE id in (\#{s.join(', ')})
|
78
|
-
UNION ALL
|
79
|
-
SELECT alias1.*, crumbs.level + 1 FROM crumbs JOIN #{self.table_name} alias1 on alias1.parent_id = crumbs.id
|
80
|
-
) SELECT count(*) as count FROM crumbs WHERE id in (\#{c.join(', ')})"
|
81
|
-
)
|
82
|
-
q.first['count'].to_i > 0
|
83
|
-
end
|
84
|
-
end
|
85
|
-
|
86
|
-
def self.nodes_within(src, chk)
|
87
|
-
s = (src.is_a?(Array) ? src : [src]).map{|x|x.is_a?(ActiveRecord::Base) ? x.id : x.to_i}
|
88
|
-
c = (chk.is_a?(Array) ? chk : [chk]).map{|x|x.is_a?(ActiveRecord::Base) ? x.id : x.to_i}
|
89
|
-
if(s.empty? || c.empty?)
|
90
|
-
nil
|
91
|
-
else
|
92
|
-
self.find_by_sql(
|
93
|
-
"WITH RECURSIVE crumbs AS (
|
94
|
-
SELECT #{self.table_name}.*, 0 AS level FROM #{self.table_name} WHERE id in (\#{s.join(', ')})
|
95
|
-
UNION ALL
|
96
|
-
SELECT alias1.*, crumbs.level + 1 FROM crumbs JOIN #{self.table_name} alias1 on alias1.parent_id = crumbs.id
|
97
|
-
#{configuration[:max_depth] ? "WHERE crumbs.level + 1 < #{configuration[:max_depth].to_i}" : ''}
|
98
|
-
) SELECT * FROM crumbs WHERE id in (\#{c.join(', ')})"
|
99
|
-
)
|
100
|
-
end
|
101
|
-
end
|
102
|
-
|
103
|
-
def self.nodes_and_descendents(*args)
|
104
|
-
raw = args.delete(:raw)
|
105
|
-
no_self = args.delete(:no_self)
|
106
|
-
at_depth = nil
|
107
|
-
depth = nil
|
108
|
-
hash = args.detect{|x|x.is_a?(Hash)}
|
109
|
-
if(hash)
|
110
|
-
args.delete(hash)
|
111
|
-
depth = hash[:depth] || hash[:to_depth]
|
112
|
-
at_depth = hash[:at_depth]
|
113
|
-
end
|
114
|
-
depth ||= #{configuration[:max_depth].to_i}
|
115
|
-
depth_restriction = "WHERE crumbs.level + 1 < \#{depth}" if depth
|
116
|
-
depth_clause = nil
|
117
|
-
if(at_depth)
|
118
|
-
depth_clause = "level + 1 = \#{at_depth.to_i}"
|
119
|
-
elsif(depth)
|
120
|
-
depth_clause = "level + 1 < \#{depth.to_i}"
|
121
|
-
end
|
122
|
-
base_ids = args.map{|x| x.is_a?(ActiveRecord::Base) ? x.id : x.to_i}
|
123
|
-
q = self.find_by_sql(
|
124
|
-
"WITH RECURSIVE crumbs AS (
|
125
|
-
SELECT #{self.table_name}.*, \#{no_self ? -1 : 0} AS level FROM #{self.table_name} WHERE \#{base_ids.empty? ? 'parent_id IS NULL' : "id in (\#{base_ids.join(', ')})"}
|
126
|
-
UNION ALL
|
127
|
-
SELECT alias1.*, crumbs.level + 1 FROM crumbs JOIN #{self.table_name} alias1 on alias1.parent_id = crumbs.id
|
128
|
-
\#{depth_restriction}
|
129
|
-
) SELECT * FROM crumbs WHERE level >= 0 \#{"AND " + depth_clause if depth_clause} ORDER BY level, parent_id ASC"
|
130
|
-
)
|
131
|
-
unless(raw)
|
132
|
-
res = {}
|
133
|
-
cache = {}
|
134
|
-
q.each do |item|
|
135
|
-
cache[item.id] = {}
|
136
|
-
if(cache[item.parent_id])
|
137
|
-
cache[item.parent_id][item] = cache[item.id]
|
138
|
-
else
|
139
|
-
res[item] = cache[item.id]
|
140
|
-
end
|
141
|
-
end
|
142
|
-
res
|
143
|
-
else
|
144
|
-
q
|
145
|
-
end
|
146
|
-
end
|
147
|
-
|
148
|
-
EOV
|
149
|
-
end
|
150
|
-
end
|
151
|
-
|
152
|
-
module InstanceMethods
|
153
|
-
|
154
|
-
# Returns all ancestors of the current node.
|
155
|
-
def ancestors
|
156
|
-
self.class.find_by_sql "WITH RECURSIVE crumbs AS (
|
157
|
-
SELECT #{self.class.table_name}.*,
|
158
|
-
1 AS level
|
159
|
-
FROM #{self.class.table_name}
|
160
|
-
WHERE id = #{id}
|
161
|
-
UNION ALL
|
162
|
-
SELECT alias1.*,
|
163
|
-
level + 1
|
164
|
-
FROM crumbs
|
165
|
-
JOIN #{self.class.table_name} alias1 ON alias1.id = crumbs.parent_id
|
166
|
-
) SELECT * FROM crumbs WHERE id != #{id} ORDER BY level DESC"
|
167
|
-
end
|
168
|
-
|
169
|
-
# Returns the root node of the tree.
|
170
|
-
def root
|
171
|
-
ancestors.first
|
172
|
-
end
|
173
|
-
|
174
|
-
# Returns all siblings of the current node.
|
175
|
-
#
|
176
|
-
# subchild1.siblings # => [subchild2]
|
177
|
-
def siblings
|
178
|
-
self_and_siblings - [self]
|
179
|
-
end
|
180
|
-
|
181
|
-
# Returns all siblings and a reference to the current node.
|
182
|
-
#
|
183
|
-
# subchild1.self_and_siblings # => [subchild1, subchild2]
|
184
|
-
def self_and_siblings
|
185
|
-
parent ? parent.children : self.class.roots
|
186
|
-
end
|
187
|
-
|
188
|
-
# Returns if the current node is a root
|
189
|
-
def root?
|
190
|
-
parent_id.nil?
|
191
|
-
end
|
192
|
-
|
193
|
-
# Returns all descendents of the current node. Each level
|
194
|
-
# is within its own hash, so for a structure like:
|
195
|
-
# root
|
196
|
-
# \_ child1
|
197
|
-
# \_ subchild1
|
198
|
-
# \_ subsubchild1
|
199
|
-
# \_ subchild2
|
200
|
-
# the resulting hash would look like:
|
201
|
-
#
|
202
|
-
# {child1 =>
|
203
|
-
# {subchild1 =>
|
204
|
-
# {subsubchild1 => {}},
|
205
|
-
# subchild2 => {}}}
|
206
|
-
#
|
207
|
-
# This method will accept two parameters.
|
208
|
-
# * :raw -> Result is flat array. No Hash tree is built
|
209
|
-
# * {:depth => n} -> Will only search for descendents to the given depth of n
|
210
|
-
def descendents(*args)
|
211
|
-
args.delete_if{|x| !x.is_a?(Hash) && x != :raw}
|
212
|
-
self.class.nodes_and_descendents(:no_self, self, *args)
|
213
|
-
end
|
214
|
-
|
215
|
-
# Returns the depth of the current node. 0 depth represents the root of the tree
|
216
|
-
def depth
|
217
|
-
res = self.class.connection.select_all(
|
218
|
-
"WITH RECURSIVE crumbs AS (
|
219
|
-
SELECT parent_id, 0 AS level
|
220
|
-
FROM #{self.class.table_name}
|
221
|
-
WHERE id = #{id}
|
222
|
-
UNION ALL
|
223
|
-
SELECT alias1.parent_id, level + 1
|
224
|
-
FROM crumbs
|
225
|
-
JOIN #{self.class.table_name} alias1 ON alias1.id = crumbs.parent_id
|
226
|
-
) SELECT level FROM crumbs ORDER BY level DESC LIMIT 1"
|
227
|
-
)
|
228
|
-
res.empty? ? nil : res.first['level']
|
229
|
-
end
|
230
|
-
end
|
4
|
+
if(defined?(Rails))
|
5
|
+
ActiveRecord::Base.send :include, ActsAsSaneTree
|
231
6
|
end
|
232
|
-
|
233
|
-
ActiveRecord::Base.send :include, ActsAsSaneTree
|
metadata
CHANGED
@@ -1,12 +1,12 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: acts_as_sane_tree
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
hash:
|
5
|
-
prerelease:
|
4
|
+
hash: 3
|
5
|
+
prerelease:
|
6
6
|
segments:
|
7
|
-
-
|
8
|
-
-
|
9
|
-
version: "
|
7
|
+
- 2
|
8
|
+
- 0
|
9
|
+
version: "2.0"
|
10
10
|
platform: ruby
|
11
11
|
authors:
|
12
12
|
- Chris Roberts
|
@@ -14,7 +14,7 @@ autorequire:
|
|
14
14
|
bindir: bin
|
15
15
|
cert_chain: []
|
16
16
|
|
17
|
-
date: 2011-
|
17
|
+
date: 2011-03-08 00:00:00 -08:00
|
18
18
|
default_executable:
|
19
19
|
dependencies:
|
20
20
|
- !ruby/object:Gem::Dependency
|
@@ -77,7 +77,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
77
77
|
requirements: []
|
78
78
|
|
79
79
|
rubyforge_project:
|
80
|
-
rubygems_version: 1.
|
80
|
+
rubygems_version: 1.5.1
|
81
81
|
signing_key:
|
82
82
|
specification_version: 3
|
83
83
|
summary: Sane tree builder for ActiveRecord and Postgresql
|