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.
@@ -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