acts_as_sane_tree 2.0 → 2.0.1
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/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
|