acts_as_sane_tree 1.4 → 2.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 +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
|