acts_as_sane_tree 2.0 → 2.0.1
Sign up to get free protection for your applications and to get access to all the features.
data/acts_as_sane_tree.gemspec
CHANGED
@@ -19,6 +19,9 @@ README.rdoc
|
|
19
19
|
CHANGELOG.rdoc
|
20
20
|
lib/acts_as_sane_tree.rb
|
21
21
|
lib/acts_as_sane_tree/version.rb
|
22
|
+
lib/acts_as_sane_tree/acts_as_sane_tree.rb
|
23
|
+
lib/acts_as_sane_tree/singleton_methods.rb
|
24
|
+
lib/acts_as_sane_tree/instance_methods.rb
|
22
25
|
rails/init.rb
|
23
26
|
}
|
24
27
|
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
require 'active_record'
|
2
|
+
require 'acts_as_sane_tree/instance_methods'
|
3
|
+
require 'acts_as_sane_tree/singleton_methods.rb'
|
4
|
+
|
5
|
+
module ActsAsSaneTree
|
6
|
+
|
7
|
+
def self.included(base) # :nodoc:
|
8
|
+
base.extend(ClassMethods)
|
9
|
+
end
|
10
|
+
|
11
|
+
# Specify this +acts_as+ extension if you want to model a tree structure by providing a parent association and a children
|
12
|
+
# association. This requires that you have a foreign key column, which by default is called +parent_id+.
|
13
|
+
#
|
14
|
+
# class Category < ActiveRecord::Base
|
15
|
+
# acts_as_sane_tree :order => "name"
|
16
|
+
# end
|
17
|
+
#
|
18
|
+
# Example:
|
19
|
+
# root
|
20
|
+
# \_ child1
|
21
|
+
# \_ subchild1
|
22
|
+
# \_ subchild2
|
23
|
+
#
|
24
|
+
# root = Category.create("name" => "root")
|
25
|
+
# child1 = root.children.create("name" => "child1")
|
26
|
+
# subchild1 = child1.children.create("name" => "subchild1")
|
27
|
+
#
|
28
|
+
# root.parent # => nil
|
29
|
+
# child1.parent # => root
|
30
|
+
# root.children # => [child1]
|
31
|
+
# root.children.first.children.first # => subchild1
|
32
|
+
#
|
33
|
+
# The following class methods are also added:
|
34
|
+
#
|
35
|
+
# * <tt>nodes_within?(src, chk)</tt> - Returns true if chk contains any nodes found within src and all ancestors of nodes within src
|
36
|
+
# * <tt>nodes_within(src, chk)</tt> - Returns any matching nodes from chk found within src and all ancestors within src
|
37
|
+
# * <tt>nodes_and_descendents(*args)</tt> - Returns all nodes and descendents for given IDs or records. Accepts multiple IDs and records. Valid options:
|
38
|
+
# * :raw - No Hash nesting
|
39
|
+
# * :no_self - Will not return given nodes in result set
|
40
|
+
# * {:depth => n} - Will set maximum depth to query
|
41
|
+
# * {:to_depth => n} - Alias for :depth
|
42
|
+
# * {:at_depth => n} - Will return times at given depth (takes precedence over :depth/:to_depth)
|
43
|
+
module ClassMethods
|
44
|
+
# Configuration options are:
|
45
|
+
#
|
46
|
+
# * <tt>foreign_key</tt> - specifies the column name to use for tracking of the tree (default: +parent_id+)
|
47
|
+
# * <tt>order</tt> - makes it possible to sort the children according to this SQL snippet.
|
48
|
+
# * <tt>counter_cache</tt> - keeps a count in a +children_count+ column if set to +true+ (default: +false+).
|
49
|
+
def acts_as_sane_tree(options = {})
|
50
|
+
@configuration = { :foreign_key => "parent_id", :order => nil, :counter_cache => nil, :max_depth => 10000, :class => self }
|
51
|
+
@configuration.update(options) if options.is_a?(Hash)
|
52
|
+
|
53
|
+
class_eval do
|
54
|
+
cattr_accessor :configuration
|
55
|
+
belongs_to :parent, :class_name => name, :foreign_key => @configuration[:foreign_key], :counter_cache => @configuration[:counter_cache]
|
56
|
+
has_many :children, :class_name => name, :foreign_key => @configuration[:foreign_key], :order => @configuration[:order], :dependent => :destroy
|
57
|
+
|
58
|
+
validates_each @configuration[:foreign_key] do |record, attr, value|
|
59
|
+
record.errors.add attr, 'cannot be own parent.' if !record.id.nil? && value.to_i == record.id.to_i
|
60
|
+
end
|
61
|
+
end
|
62
|
+
self.configuration = @configuration
|
63
|
+
include ActsAsSaneTree::InstanceMethods
|
64
|
+
extend ActsAsSaneTree::SingletonMethods
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,114 @@
|
|
1
|
+
module ActsAsSaneTree
|
2
|
+
module InstanceMethods
|
3
|
+
|
4
|
+
# Returns all ancestors of the current node.
|
5
|
+
def ancestors
|
6
|
+
query =
|
7
|
+
"(WITH RECURSIVE crumbs AS (
|
8
|
+
SELECT #{self.class.configuration[:class].table_name}.*,
|
9
|
+
1 AS depth
|
10
|
+
FROM #{self.class.configuration[:class].table_name}
|
11
|
+
WHERE id = #{id}
|
12
|
+
UNION ALL
|
13
|
+
SELECT alias1.*,
|
14
|
+
depth + 1
|
15
|
+
FROM crumbs
|
16
|
+
JOIN #{self.class.configuration[:class].table_name} alias1 ON alias1.id = crumbs.parent_id
|
17
|
+
) SELECT * FROM crumbs WHERE crumbs.id != #{id}) as #{self.class.configuration[:class].table_name}"
|
18
|
+
if(self.class.rails_3?)
|
19
|
+
self.class.configuration[:class].send(:with_exclusive_scope) do
|
20
|
+
self.class.configuration[:class].from(
|
21
|
+
query
|
22
|
+
).order("#{self.class.configuration[:class].table_name}.depth DESC")
|
23
|
+
end
|
24
|
+
else
|
25
|
+
self.class.configuration[:class].send(:with_exclusive_scope) do
|
26
|
+
self.class.configuration[:class].scoped(
|
27
|
+
:from => query,
|
28
|
+
:order => "#{self.class.configuration[:class].table_name}.depth DESC"
|
29
|
+
)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
# Returns the root node of the tree.
|
35
|
+
def root
|
36
|
+
ancestors.first
|
37
|
+
end
|
38
|
+
|
39
|
+
# Returns all siblings of the current node.
|
40
|
+
#
|
41
|
+
# subchild1.siblings # => [subchild2]
|
42
|
+
def siblings
|
43
|
+
self_and_siblings - [self]
|
44
|
+
end
|
45
|
+
|
46
|
+
# Returns all siblings and a reference to the current node.
|
47
|
+
#
|
48
|
+
# subchild1.self_and_siblings # => [subchild1, subchild2]
|
49
|
+
def self_and_siblings
|
50
|
+
parent ? parent.children : self.class.configuration[:class].roots
|
51
|
+
end
|
52
|
+
|
53
|
+
# Returns if the current node is a root
|
54
|
+
def root?
|
55
|
+
parent_id.nil?
|
56
|
+
end
|
57
|
+
|
58
|
+
# Returns all descendents of the current node. Each level
|
59
|
+
# is within its own hash, so for a structure like:
|
60
|
+
# root
|
61
|
+
# \_ child1
|
62
|
+
# \_ subchild1
|
63
|
+
# \_ subsubchild1
|
64
|
+
# \_ subchild2
|
65
|
+
# the resulting hash would look like:
|
66
|
+
#
|
67
|
+
# {child1 =>
|
68
|
+
# {subchild1 =>
|
69
|
+
# {subsubchild1 => {}},
|
70
|
+
# subchild2 => {}}}
|
71
|
+
#
|
72
|
+
# This method will accept two parameters.
|
73
|
+
# * :raw -> Result is scope that can more finders can be chained against with additional 'level' attribute
|
74
|
+
# * {:depth => n} -> Will only search for descendents to the given depth of n
|
75
|
+
# NOTE: You can restrict results by depth on the scope returned, but better performance will be
|
76
|
+
# gained by specifying it within the args so it will be applied during the recursion, not after.
|
77
|
+
def descendents(*args)
|
78
|
+
args.delete_if{|x| !x.is_a?(Hash) && x != :raw}
|
79
|
+
self.class.configuration[:class].nodes_and_descendents(:no_self, self, *args)
|
80
|
+
end
|
81
|
+
|
82
|
+
# Returns the depth of the current node. 0 depth represents the root of the tree
|
83
|
+
def depth
|
84
|
+
query =
|
85
|
+
"(WITH RECURSIVE crumbs AS (
|
86
|
+
SELECT parent_id, 0 AS depth
|
87
|
+
FROM #{self.class.configuration[:class].table_name}
|
88
|
+
WHERE id = #{id}
|
89
|
+
UNION ALL
|
90
|
+
SELECT alias1.parent_id, level + 1
|
91
|
+
FROM crumbs
|
92
|
+
JOIN #{self.class.configuration[:class].table_name} alias1 ON alias1.id = crumbs.parent_id
|
93
|
+
) SELECT depth FROM crumbs) as #{self.class.configuration[:class].table_name}"
|
94
|
+
if(self.class.rails_3?)
|
95
|
+
self.class.configuration[:class].send(:with_exclusive_scope) do
|
96
|
+
self.class.configuration[:class].from(
|
97
|
+
query
|
98
|
+
).order(
|
99
|
+
"#{self.class.configuration[:class].table_name}.depth DESC"
|
100
|
+
).limit(1).try(:first).try(:depth)
|
101
|
+
end
|
102
|
+
else
|
103
|
+
self.class.configuration[:class].send(:with_exclusive_scope) do
|
104
|
+
self.class.configuration[:class].find(
|
105
|
+
:first,
|
106
|
+
:from => query,
|
107
|
+
:order => "#{self.class.configuration[:class].table_name}.depth DESC",
|
108
|
+
:limit => 1
|
109
|
+
).try(:depth)
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
@@ -0,0 +1,157 @@
|
|
1
|
+
module ActsAsSaneTree
|
2
|
+
module SingletonMethods
|
3
|
+
|
4
|
+
# Check if we are in rails 3
|
5
|
+
def rails_3?
|
6
|
+
@is_3 ||= Rails.version.split('.').first == '3'
|
7
|
+
end
|
8
|
+
|
9
|
+
# Return all root nodes
|
10
|
+
def roots
|
11
|
+
if(rails_3?)
|
12
|
+
configuration[:class].where(
|
13
|
+
"#{configuration[:foreign_key]} IS NULL"
|
14
|
+
).order(configuration[:order])
|
15
|
+
else
|
16
|
+
configuration[:class].scoped(
|
17
|
+
:conditions => "#{configuration[:foreign_key]} IS NULL",
|
18
|
+
:order => configuration[:order]
|
19
|
+
)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
# Return first root node
|
24
|
+
def root
|
25
|
+
if(rails_3?)
|
26
|
+
configuration[:class].where("#{configuration[:foriegn_key]} IS NULL").order(configuration[:order]).first
|
27
|
+
else
|
28
|
+
configuration[:class].find(
|
29
|
+
:first,
|
30
|
+
:conditions => "#{configuration[:foreign_key]} IS NULL",
|
31
|
+
:order => configuration[:order]
|
32
|
+
)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
# src:: Array of nodes
|
37
|
+
# chk:: Array of nodes
|
38
|
+
# Return true if any nodes within chk are found within src
|
39
|
+
def nodes_within?(src, chk)
|
40
|
+
s = (src.is_a?(Array) ? src : [src]).map{|x|x.is_a?(ActiveRecord::Base) ? x.id : x.to_i}
|
41
|
+
c = (chk.is_a?(Array) ? chk : [chk]).map{|x|x.is_a?(ActiveRecord::Base) ? x.id : x.to_i}
|
42
|
+
if(s.empty? || c.empty?)
|
43
|
+
false
|
44
|
+
else
|
45
|
+
q = configuration[:class].connection.select_all(
|
46
|
+
"WITH RECURSIVE crumbs AS (
|
47
|
+
SELECT #{configuration[:class].table_name}.*, 0 AS level FROM #{configuration[:class].table_name} WHERE id in (#{s.join(', ')})
|
48
|
+
UNION ALL
|
49
|
+
SELECT alias1.*, crumbs.level + 1 FROM crumbs JOIN #{configuration[:class].table_name} alias1 on alias1.parent_id = crumbs.id
|
50
|
+
) SELECT count(*) as count FROM crumbs WHERE id in (#{c.join(', ')})"
|
51
|
+
)
|
52
|
+
q.first['count'].to_i > 0
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
# src:: Array of nodes
|
57
|
+
# chk:: Array of nodes
|
58
|
+
# Return all nodes that are within both chk and src
|
59
|
+
def 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
|
+
nil
|
64
|
+
else
|
65
|
+
query =
|
66
|
+
"(WITH RECURSIVE crumbs AS (
|
67
|
+
SELECT #{configuration[:class].table_name}.*, 0 AS depth FROM #{configuration[:class].table_name} WHERE id in (\#{s.join(', ')})
|
68
|
+
UNION ALL
|
69
|
+
SELECT alias1.*, crumbs.depth + 1 FROM crumbs JOIN #{configuration[:class].table_name} alias1 on alias1.parent_id = crumbs.id
|
70
|
+
#{configuration[:max_depth] ? "WHERE crumbs.depth + 1 < #{configuration[:max_depth].to_i}" : ''}
|
71
|
+
) SELECT * FROM crumbs WHERE id in (#{c.join(', ')})) as #{configuration[:class].table_name}"
|
72
|
+
if(rails_3?)
|
73
|
+
configuration[:class].from(query)
|
74
|
+
else
|
75
|
+
configuration[:class].scoped(:from => query)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
# args:: ActiveRecord models or IDs - Symbols: :raw, :no_self - Hash: {:to_depth => n, :at_depth => n}
|
81
|
+
# Returns provided nodes plus all descendents of provided nodes in nested Hash where keys are nodes and values are children
|
82
|
+
# :raw:: return value will be flat array
|
83
|
+
# :no_self:: Do not include provided nodes in result
|
84
|
+
# Hash:
|
85
|
+
# :to_depth:: Only retrieve values to given depth
|
86
|
+
# :at_depth:: Only retrieve values from given depth
|
87
|
+
def nodes_and_descendents(*args)
|
88
|
+
raw = args.delete(:raw)
|
89
|
+
no_self = args.delete(:no_self)
|
90
|
+
at_depth = nil
|
91
|
+
depth = nil
|
92
|
+
hash = args.detect{|x|x.is_a?(Hash)}
|
93
|
+
if(hash)
|
94
|
+
args.delete(hash)
|
95
|
+
depth = hash[:depth] || hash[:to_depth]
|
96
|
+
at_depth = hash[:at_depth]
|
97
|
+
end
|
98
|
+
depth ||= configuration[:max_depth].to_i
|
99
|
+
depth_restriction = "WHERE crumbs.depth + 1 < #{depth}" if depth
|
100
|
+
depth_clause = nil
|
101
|
+
if(at_depth)
|
102
|
+
depth_clause = "#{configuration[:class].table_name}.depth + 1 = #{at_depth.to_i}"
|
103
|
+
elsif(depth)
|
104
|
+
depth_clause = "#{configuration[:class].table_name}.depth + 1 < #{depth.to_i}"
|
105
|
+
end
|
106
|
+
base_ids = args.map{|x| x.is_a?(ActiveRecord::Base) ? x.id : x.to_i}
|
107
|
+
query =
|
108
|
+
"(WITH RECURSIVE crumbs AS (
|
109
|
+
SELECT #{configuration[:class].table_name}.*, #{no_self ? -1 : 0} AS depth FROM #{configuration[:class].table_name} WHERE #{base_ids.empty? ? 'parent_id IS NULL' : "id in (#{base_ids.join(', ')})"}
|
110
|
+
UNION ALL
|
111
|
+
SELECT alias1.*, crumbs.depth + 1 FROM crumbs JOIN #{configuration[:class].table_name} alias1 on alias1.parent_id = crumbs.id
|
112
|
+
#{depth_restriction}
|
113
|
+
) SELECT * FROM crumbs) as #{configuration[:class].table_name}"
|
114
|
+
q = nil
|
115
|
+
if(rails_3?)
|
116
|
+
with_exclusive_scope do
|
117
|
+
q = configuration[:class].from(
|
118
|
+
query
|
119
|
+
).where(
|
120
|
+
"#{configuration[:class].table_name}.depth >= 0"
|
121
|
+
)
|
122
|
+
if(depth_clause)
|
123
|
+
q = q.where(depth_clause)
|
124
|
+
end
|
125
|
+
q = q.order("#{configuration[:class].table_name}.depth ASC, #{configuration[:class].table_name}.parent_id ASC")
|
126
|
+
end
|
127
|
+
else
|
128
|
+
with_exclusive_scope do
|
129
|
+
q = configuration[:class].scoped(
|
130
|
+
:from => query,
|
131
|
+
:conditions => "#{configuration[:class].table_name}.depth >= 0",
|
132
|
+
:order => "#{configuration[:class].table_name}.depth ASC, #{configuration[:class].table_name}.parent_id ASC"
|
133
|
+
)
|
134
|
+
if(depth_clause)
|
135
|
+
q = q.scoped(:conditions => depth_clause)
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
139
|
+
unless(raw)
|
140
|
+
res = {}
|
141
|
+
cache = {}
|
142
|
+
q.all.each do |item|
|
143
|
+
cache[item.id] = {}
|
144
|
+
if(cache[item.parent_id])
|
145
|
+
cache[item.parent_id][item] = cache[item.id]
|
146
|
+
else
|
147
|
+
res[item] = cache[item.id]
|
148
|
+
end
|
149
|
+
end
|
150
|
+
res
|
151
|
+
else
|
152
|
+
q
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
end
|
157
|
+
end
|
metadata
CHANGED
@@ -1,12 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: acts_as_sane_tree
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
hash:
|
4
|
+
hash: 13
|
5
5
|
prerelease:
|
6
6
|
segments:
|
7
7
|
- 2
|
8
8
|
- 0
|
9
|
-
|
9
|
+
- 1
|
10
|
+
version: 2.0.1
|
10
11
|
platform: ruby
|
11
12
|
authors:
|
12
13
|
- Chris Roberts
|
@@ -46,6 +47,9 @@ files:
|
|
46
47
|
- CHANGELOG.rdoc
|
47
48
|
- lib/acts_as_sane_tree.rb
|
48
49
|
- lib/acts_as_sane_tree/version.rb
|
50
|
+
- lib/acts_as_sane_tree/acts_as_sane_tree.rb
|
51
|
+
- lib/acts_as_sane_tree/singleton_methods.rb
|
52
|
+
- lib/acts_as_sane_tree/instance_methods.rb
|
49
53
|
- rails/init.rb
|
50
54
|
has_rdoc: true
|
51
55
|
homepage: http://github.com/chrisroberts/acts_as_sane_tree
|