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 +39 -0
- data/acts_as_sane_tree.gemspec +22 -0
- data/init.rb +1 -0
- data/lib/acts_as_sane_tree.rb +202 -0
- data/lib/version.rb +3 -0
- metadata +83 -0
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
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
|
+
|