awesome_nested_set 2.1.6 → 3.0.0.rc.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/README.md +163 -0
- data/lib/awesome_nested_set.rb +1 -1
- data/lib/awesome_nested_set/awesome_nested_set.rb +59 -692
- data/lib/awesome_nested_set/columns.rb +72 -0
- data/lib/awesome_nested_set/helper.rb +0 -45
- data/lib/awesome_nested_set/iterator.rb +29 -0
- data/lib/awesome_nested_set/model.rb +212 -0
- data/lib/awesome_nested_set/model/movable.rb +137 -0
- data/lib/awesome_nested_set/model/prunable.rb +58 -0
- data/lib/awesome_nested_set/model/rebuildable.rb +40 -0
- data/lib/awesome_nested_set/model/relatable.rb +121 -0
- data/lib/awesome_nested_set/model/transactable.rb +27 -0
- data/lib/awesome_nested_set/model/validatable.rb +69 -0
- data/lib/awesome_nested_set/move.rb +117 -0
- data/lib/awesome_nested_set/set_validator.rb +63 -0
- data/lib/awesome_nested_set/tree.rb +63 -0
- data/lib/awesome_nested_set/version.rb +1 -1
- metadata +44 -27
- data/README.rdoc +0 -153
@@ -0,0 +1,72 @@
|
|
1
|
+
# Mixed into both classes and instances to provide easy access to the column names
|
2
|
+
module CollectiveIdea #:nodoc:
|
3
|
+
module Acts #:nodoc:
|
4
|
+
module NestedSet #:nodoc:
|
5
|
+
module Columns
|
6
|
+
def left_column_name
|
7
|
+
acts_as_nested_set_options[:left_column]
|
8
|
+
end
|
9
|
+
|
10
|
+
def right_column_name
|
11
|
+
acts_as_nested_set_options[:right_column]
|
12
|
+
end
|
13
|
+
|
14
|
+
def depth_column_name
|
15
|
+
acts_as_nested_set_options[:depth_column]
|
16
|
+
end
|
17
|
+
|
18
|
+
def parent_column_name
|
19
|
+
acts_as_nested_set_options[:parent_column]
|
20
|
+
end
|
21
|
+
|
22
|
+
def order_column
|
23
|
+
acts_as_nested_set_options[:order_column] || left_column_name
|
24
|
+
end
|
25
|
+
|
26
|
+
def scope_column_names
|
27
|
+
Array(acts_as_nested_set_options[:scope])
|
28
|
+
end
|
29
|
+
|
30
|
+
def quoted_left_column_name
|
31
|
+
ActiveRecord::Base.connection.quote_column_name(left_column_name)
|
32
|
+
end
|
33
|
+
|
34
|
+
def quoted_right_column_name
|
35
|
+
ActiveRecord::Base.connection.quote_column_name(right_column_name)
|
36
|
+
end
|
37
|
+
|
38
|
+
def quoted_depth_column_name
|
39
|
+
ActiveRecord::Base.connection.quote_column_name(depth_column_name)
|
40
|
+
end
|
41
|
+
|
42
|
+
def quoted_parent_column_name
|
43
|
+
ActiveRecord::Base.connection.quote_column_name(parent_column_name)
|
44
|
+
end
|
45
|
+
|
46
|
+
def quoted_scope_column_names
|
47
|
+
scope_column_names.collect {|column_name| connection.quote_column_name(column_name) }
|
48
|
+
end
|
49
|
+
|
50
|
+
def quoted_order_column_name
|
51
|
+
ActiveRecord::Base.connection.quote_column_name(order_column)
|
52
|
+
end
|
53
|
+
|
54
|
+
def quoted_primary_key_column_full_name
|
55
|
+
"#{quoted_table_name}.#{ActiveRecord::Base.connection.quote_column_name('id')}"
|
56
|
+
end
|
57
|
+
|
58
|
+
def quoted_left_column_full_name
|
59
|
+
"#{quoted_table_name}.#{quoted_left_column_name}"
|
60
|
+
end
|
61
|
+
|
62
|
+
def quoted_right_column_full_name
|
63
|
+
"#{quoted_table_name}.#{quoted_right_column_name}"
|
64
|
+
end
|
65
|
+
|
66
|
+
def quoted_parent_column_full_name
|
67
|
+
"#{quoted_table_name}.#{quoted_parent_column_name}"
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
@@ -38,51 +38,6 @@ module CollectiveIdea #:nodoc:
|
|
38
38
|
end
|
39
39
|
result
|
40
40
|
end
|
41
|
-
|
42
|
-
# Returns options for select as nested_set_options, sorted by an specific column
|
43
|
-
# It requires passing a string with the name of the column to sort the set with
|
44
|
-
# You can exclude some items from the tree.
|
45
|
-
# You can pass a block receiving an item and returning the string displayed in the select.
|
46
|
-
#
|
47
|
-
# == Params
|
48
|
-
# * +class_or_item+ - Class name or top level times
|
49
|
-
# * +:column+ - Column to sort the set (this will sort each children for all root elements)
|
50
|
-
# * +mover+ - The item that is being move, used to exlude impossible moves
|
51
|
-
# * +&block+ - a block that will be used to display: { |item| ... item.name }
|
52
|
-
#
|
53
|
-
# == Usage
|
54
|
-
#
|
55
|
-
# <%= f.select :parent_id, nested_set_options(Category, :sort_by_this_column, @category) {|i|
|
56
|
-
# "#{'–' * i.level} #{i.name}"
|
57
|
-
# }) %>
|
58
|
-
#
|
59
|
-
def sorted_nested_set_options(class_or_item, order, mover = nil)
|
60
|
-
if class_or_item.is_a? Array
|
61
|
-
items = class_or_item.reject { |e| !e.root? }
|
62
|
-
else
|
63
|
-
class_or_item = class_or_item.roots if class_or_item.is_a?(Class)
|
64
|
-
items = Array(class_or_item)
|
65
|
-
end
|
66
|
-
result = []
|
67
|
-
children = []
|
68
|
-
items.each do |root|
|
69
|
-
root.class.associate_parents(root.self_and_descendants).map do |i|
|
70
|
-
if mover.nil? || mover.new_record? || mover.move_possible?(i)
|
71
|
-
if !i.leaf?
|
72
|
-
children.sort_by! &order
|
73
|
-
children.each { |c| result << [yield(c), c.id] }
|
74
|
-
children = []
|
75
|
-
result << [yield(i), i.id]
|
76
|
-
else
|
77
|
-
children << i
|
78
|
-
end
|
79
|
-
end
|
80
|
-
end.compact
|
81
|
-
end
|
82
|
-
children.sort_by! &order
|
83
|
-
children.each { |c| result << [yield(c), c.id] }
|
84
|
-
result
|
85
|
-
end
|
86
41
|
end
|
87
42
|
end
|
88
43
|
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module CollectiveIdea #:nodoc:
|
2
|
+
module Acts #:nodoc:
|
3
|
+
module NestedSet #:nodoc:
|
4
|
+
class Iterator
|
5
|
+
attr_reader :objects
|
6
|
+
|
7
|
+
def initialize(objects)
|
8
|
+
@objects = objects
|
9
|
+
end
|
10
|
+
|
11
|
+
def each_with_level
|
12
|
+
path = [nil]
|
13
|
+
objects.each do |o|
|
14
|
+
if o.parent_id != path.last
|
15
|
+
# we are on a new level, did we descend or ascend?
|
16
|
+
if path.include?(o.parent_id)
|
17
|
+
# remove wrong tailing paths elements
|
18
|
+
path.pop while path.last != o.parent_id
|
19
|
+
else
|
20
|
+
path << o.parent_id
|
21
|
+
end
|
22
|
+
end
|
23
|
+
yield(o, path.length - 1)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,212 @@
|
|
1
|
+
require 'awesome_nested_set/model/prunable'
|
2
|
+
require 'awesome_nested_set/model/movable'
|
3
|
+
require 'awesome_nested_set/model/transactable'
|
4
|
+
require 'awesome_nested_set/model/relatable'
|
5
|
+
require 'awesome_nested_set/model/rebuildable'
|
6
|
+
require 'awesome_nested_set/model/validatable'
|
7
|
+
require 'awesome_nested_set/iterator'
|
8
|
+
|
9
|
+
module CollectiveIdea #:nodoc:
|
10
|
+
module Acts #:nodoc:
|
11
|
+
module NestedSet #:nodoc:
|
12
|
+
|
13
|
+
module Model
|
14
|
+
extend ActiveSupport::Concern
|
15
|
+
|
16
|
+
included do
|
17
|
+
delegate :quoted_table_name, :arel_table, :to => self
|
18
|
+
extend Validatable
|
19
|
+
extend Rebuildable
|
20
|
+
include Movable
|
21
|
+
include Prunable
|
22
|
+
include Relatable
|
23
|
+
include Transactable
|
24
|
+
end
|
25
|
+
|
26
|
+
module ClassMethods
|
27
|
+
def associate_parents(objects)
|
28
|
+
return objects unless objects.all? {|o| o.respond_to?(:association)}
|
29
|
+
|
30
|
+
id_indexed = objects.index_by(&:id)
|
31
|
+
objects.each do |object|
|
32
|
+
association = object.association(:parent)
|
33
|
+
parent = id_indexed[object.parent_id]
|
34
|
+
|
35
|
+
if !association.loaded? && parent
|
36
|
+
association.target = parent
|
37
|
+
association.set_inverse_instance(parent)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def children_of(parent_id)
|
43
|
+
where arel_table[parent_column_name].eq(parent_id)
|
44
|
+
end
|
45
|
+
|
46
|
+
# Iterates over tree elements and determines the current level in the tree.
|
47
|
+
# Only accepts default ordering, odering by an other column than lft
|
48
|
+
# does not work. This method is much more efficent than calling level
|
49
|
+
# because it doesn't require any additional database queries.
|
50
|
+
#
|
51
|
+
# Example:
|
52
|
+
# Category.each_with_level(Category.root.self_and_descendants) do |o, level|
|
53
|
+
#
|
54
|
+
def each_with_level(objects, &block)
|
55
|
+
Iterator.new(objects).each_with_level(&block)
|
56
|
+
end
|
57
|
+
|
58
|
+
def leaves
|
59
|
+
nested_set_scope.where "#{quoted_right_column_full_name} - #{quoted_left_column_full_name} = 1"
|
60
|
+
end
|
61
|
+
|
62
|
+
def left_of(node)
|
63
|
+
where arel_table[left_column_name].lt(node)
|
64
|
+
end
|
65
|
+
|
66
|
+
def left_of_right_side(node)
|
67
|
+
where arel_table[right_column_name].lteq(node)
|
68
|
+
end
|
69
|
+
|
70
|
+
def right_of(node)
|
71
|
+
where arel_table[left_column_name].gteq(node)
|
72
|
+
end
|
73
|
+
|
74
|
+
def nested_set_scope(options = {})
|
75
|
+
options = {:order => quoted_order_column_name}.merge(options)
|
76
|
+
|
77
|
+
where(options[:conditions]).order(options.delete(:order))
|
78
|
+
end
|
79
|
+
|
80
|
+
def primary_key_scope(id)
|
81
|
+
where arel_table[primary_key].eq(id)
|
82
|
+
end
|
83
|
+
|
84
|
+
def root
|
85
|
+
roots.first
|
86
|
+
end
|
87
|
+
|
88
|
+
def roots
|
89
|
+
nested_set_scope.children_of nil
|
90
|
+
end
|
91
|
+
end # end class methods
|
92
|
+
|
93
|
+
# Any instance method that returns a collection makes use of Rails 2.1's named_scope (which is bundled for Rails 2.0), so it can be treated as a finder.
|
94
|
+
#
|
95
|
+
# category.self_and_descendants.count
|
96
|
+
# category.ancestors.find(:all, :conditions => "name like '%foo%'")
|
97
|
+
# Value of the parent column
|
98
|
+
def parent_id(target = self)
|
99
|
+
target[parent_column_name]
|
100
|
+
end
|
101
|
+
|
102
|
+
# Value of the left column
|
103
|
+
def left(target = self)
|
104
|
+
target[left_column_name]
|
105
|
+
end
|
106
|
+
|
107
|
+
# Value of the right column
|
108
|
+
def right(target = self)
|
109
|
+
target[right_column_name]
|
110
|
+
end
|
111
|
+
|
112
|
+
# Returns true if this is a root node.
|
113
|
+
def root?
|
114
|
+
parent_id.nil?
|
115
|
+
end
|
116
|
+
|
117
|
+
# Returns true is this is a child node
|
118
|
+
def child?
|
119
|
+
!root?
|
120
|
+
end
|
121
|
+
|
122
|
+
# Returns true if this is the end of a branch.
|
123
|
+
def leaf?
|
124
|
+
persisted? && right.to_i - left.to_i == 1
|
125
|
+
end
|
126
|
+
|
127
|
+
# All nested set queries should use this nested_set_scope, which
|
128
|
+
# performs finds on the base ActiveRecord class, using the :scope
|
129
|
+
# declared in the acts_as_nested_set declaration.
|
130
|
+
def nested_set_scope(options = {})
|
131
|
+
if (scopes = Array(acts_as_nested_set_options[:scope])).any?
|
132
|
+
options[:conditions] = scopes.inject({}) do |conditions,attr|
|
133
|
+
conditions.merge attr => self[attr]
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
self.class.nested_set_scope options
|
138
|
+
end
|
139
|
+
|
140
|
+
def to_text
|
141
|
+
self_and_descendants.map do |node|
|
142
|
+
"#{'*'*(node.level+1)} #{node.id} #{node.to_s} (#{node.parent_id}, #{node.left}, #{node.right})"
|
143
|
+
end.join("\n")
|
144
|
+
end
|
145
|
+
|
146
|
+
protected
|
147
|
+
|
148
|
+
def without_self(scope)
|
149
|
+
return scope if new_record?
|
150
|
+
scope.where(["#{self.class.quoted_table_name}.#{self.class.primary_key} != ?", self])
|
151
|
+
end
|
152
|
+
|
153
|
+
def store_new_parent
|
154
|
+
@move_to_new_parent_id = send("#{parent_column_name}_changed?") ? parent_id : false
|
155
|
+
true # force callback to return true
|
156
|
+
end
|
157
|
+
|
158
|
+
def has_depth_column?
|
159
|
+
nested_set_scope.column_names.map(&:to_s).include?(depth_column_name.to_s)
|
160
|
+
end
|
161
|
+
|
162
|
+
def right_most_node
|
163
|
+
@right_most_node ||= self.class.base_class.unscoped.nested_set_scope(
|
164
|
+
:order => "#{quoted_right_column_full_name} desc"
|
165
|
+
).first
|
166
|
+
end
|
167
|
+
|
168
|
+
def right_most_bound
|
169
|
+
@right_most_bound ||= begin
|
170
|
+
return 0 if right_most_node.nil?
|
171
|
+
|
172
|
+
right_most_node.lock!
|
173
|
+
right_most_node[right_column_name] || 0
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
177
|
+
def set_depth!
|
178
|
+
return unless has_depth_column?
|
179
|
+
|
180
|
+
in_tenacious_transaction do
|
181
|
+
reload
|
182
|
+
nested_set_scope.primary_key_scope(id).
|
183
|
+
update_all(["#{quoted_depth_column_name} = ?", level])
|
184
|
+
end
|
185
|
+
self[depth_column_name] = self.level
|
186
|
+
end
|
187
|
+
|
188
|
+
def set_default_left_and_right
|
189
|
+
# adds the new node to the right of all existing nodes
|
190
|
+
self[left_column_name] = right_most_bound + 1
|
191
|
+
self[right_column_name] = right_most_bound + 2
|
192
|
+
end
|
193
|
+
|
194
|
+
# reload left, right, and parent
|
195
|
+
def reload_nested_set
|
196
|
+
reload(
|
197
|
+
:select => "#{quoted_left_column_full_name}, #{quoted_right_column_full_name}, #{quoted_parent_column_full_name}",
|
198
|
+
:lock => true
|
199
|
+
)
|
200
|
+
end
|
201
|
+
|
202
|
+
def reload_target(target)
|
203
|
+
if target.is_a? self.class.base_class
|
204
|
+
target.reload
|
205
|
+
else
|
206
|
+
nested_set_scope.find(target)
|
207
|
+
end
|
208
|
+
end
|
209
|
+
end
|
210
|
+
end
|
211
|
+
end
|
212
|
+
end
|
@@ -0,0 +1,137 @@
|
|
1
|
+
require 'awesome_nested_set/move'
|
2
|
+
|
3
|
+
module CollectiveIdea #:nodoc:
|
4
|
+
module Acts #:nodoc:
|
5
|
+
module NestedSet #:nodoc:
|
6
|
+
module Model
|
7
|
+
module Movable
|
8
|
+
|
9
|
+
def move_possible?(target)
|
10
|
+
self != target && # Can't target self
|
11
|
+
same_scope?(target) && # can't be in different scopes
|
12
|
+
# detect impossible move
|
13
|
+
within_bounds?(target.left, target.left) &&
|
14
|
+
within_bounds?(target.right, target.right)
|
15
|
+
end
|
16
|
+
|
17
|
+
# Shorthand method for finding the left sibling and moving to the left of it.
|
18
|
+
def move_left
|
19
|
+
move_to_left_of left_sibling
|
20
|
+
end
|
21
|
+
|
22
|
+
# Shorthand method for finding the right sibling and moving to the right of it.
|
23
|
+
def move_right
|
24
|
+
move_to_right_of right_sibling
|
25
|
+
end
|
26
|
+
|
27
|
+
# Move the node to the left of another node
|
28
|
+
def move_to_left_of(node)
|
29
|
+
move_to node, :left
|
30
|
+
end
|
31
|
+
|
32
|
+
# Move the node to the left of another node
|
33
|
+
def move_to_right_of(node)
|
34
|
+
move_to node, :right
|
35
|
+
end
|
36
|
+
|
37
|
+
# Move the node to the child of another node
|
38
|
+
def move_to_child_of(node)
|
39
|
+
move_to node, :child
|
40
|
+
end
|
41
|
+
|
42
|
+
# Move the node to the child of another node with specify index
|
43
|
+
def move_to_child_with_index(node, index)
|
44
|
+
if node.children.empty?
|
45
|
+
move_to_child_of(node)
|
46
|
+
elsif node.children.count == index
|
47
|
+
move_to_right_of(node.children.last)
|
48
|
+
else
|
49
|
+
move_to_left_of(node.children[index])
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
# Move the node to root nodes
|
54
|
+
def move_to_root
|
55
|
+
move_to_right_of(root)
|
56
|
+
end
|
57
|
+
|
58
|
+
# Order children in a nested set by an attribute
|
59
|
+
# Can order by any attribute class that uses the Comparable mixin, for example a string or integer
|
60
|
+
# Usage example when sorting categories alphabetically: @new_category.move_to_ordered_child_of(@root, "name")
|
61
|
+
def move_to_ordered_child_of(parent, order_attribute, ascending = true)
|
62
|
+
self.move_to_root and return unless parent
|
63
|
+
|
64
|
+
left_neighbor = find_left_neighbor(parent, order_attribute, ascending)
|
65
|
+
self.move_to_child_of(parent)
|
66
|
+
|
67
|
+
return unless parent.children.many?
|
68
|
+
|
69
|
+
if left_neighbor
|
70
|
+
self.move_to_right_of(left_neighbor)
|
71
|
+
else # Self is the left most node.
|
72
|
+
self.move_to_left_of(parent.children[0])
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
# Find the node immediately to the left of this node.
|
77
|
+
def find_left_neighbor(parent, order_attribute, ascending)
|
78
|
+
left = nil
|
79
|
+
parent.children.each do |n|
|
80
|
+
if ascending
|
81
|
+
left = n if n.send(order_attribute) < self.send(order_attribute)
|
82
|
+
else
|
83
|
+
left = n if n.send(order_attribute) > self.send(order_attribute)
|
84
|
+
end
|
85
|
+
end
|
86
|
+
left
|
87
|
+
end
|
88
|
+
|
89
|
+
def move_to(target, position)
|
90
|
+
prevent_unpersisted_move
|
91
|
+
|
92
|
+
run_callbacks :move do
|
93
|
+
in_tenacious_transaction do
|
94
|
+
target = reload_target(target)
|
95
|
+
self.reload_nested_set
|
96
|
+
|
97
|
+
Move.new(target, position, self).move
|
98
|
+
end
|
99
|
+
after_move_to(target, position)
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
protected
|
104
|
+
|
105
|
+
def after_move_to(target, position)
|
106
|
+
target.reload_nested_set if target
|
107
|
+
self.set_depth!
|
108
|
+
self.descendants.each(&:save)
|
109
|
+
self.reload_nested_set
|
110
|
+
end
|
111
|
+
|
112
|
+
def move_to_new_parent
|
113
|
+
if @move_to_new_parent_id.nil?
|
114
|
+
move_to_root
|
115
|
+
elsif @move_to_new_parent_id
|
116
|
+
move_to_child_of(@move_to_new_parent_id)
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
def out_of_bounds?(left_bound, right_bound)
|
121
|
+
left <= left_bound && right >= right_bound
|
122
|
+
end
|
123
|
+
|
124
|
+
def prevent_unpersisted_move
|
125
|
+
if self.new_record?
|
126
|
+
raise ActiveRecord::ActiveRecordError, "You cannot move a new node"
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
def within_bounds?(left_bound, right_bound)
|
131
|
+
!out_of_bounds?(left_bound, right_bound)
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|