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.
@@ -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
@@ -1,3 +1,3 @@
1
1
  module ActsAsSaneTree
2
- VERSION = '2.0'
2
+ VERSION = '2.0.1'
3
3
  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: 3
4
+ hash: 13
5
5
  prerelease:
6
6
  segments:
7
7
  - 2
8
8
  - 0
9
- version: "2.0"
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