awesome_nested_set 2.1.6 → 3.0.0.rc.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.
- 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
|