loyal_awesome_nested_set 0.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/CHANGELOG +57 -0
- data/MIT-LICENSE +20 -0
- data/README.md +163 -0
- data/lib/awesome_nested_set.rb +8 -0
- data/lib/awesome_nested_set/awesome_nested_set.rb +134 -0
- data/lib/awesome_nested_set/columns.rb +72 -0
- data/lib/awesome_nested_set/helper.rb +44 -0
- 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 +3 -0
- metadata +151 -0
@@ -0,0 +1,44 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
module CollectiveIdea #:nodoc:
|
3
|
+
module Acts #:nodoc:
|
4
|
+
module NestedSet #:nodoc:
|
5
|
+
# This module provides some helpers for the model classes using acts_as_nested_set.
|
6
|
+
# It is included by default in all views.
|
7
|
+
#
|
8
|
+
module Helper
|
9
|
+
# Returns options for select.
|
10
|
+
# You can exclude some items from the tree.
|
11
|
+
# You can pass a block receiving an item and returning the string displayed in the select.
|
12
|
+
#
|
13
|
+
# == Params
|
14
|
+
# * +class_or_item+ - Class name or top level times
|
15
|
+
# * +mover+ - The item that is being move, used to exlude impossible moves
|
16
|
+
# * +&block+ - a block that will be used to display: { |item| ... item.name }
|
17
|
+
#
|
18
|
+
# == Usage
|
19
|
+
#
|
20
|
+
# <%= f.select :parent_id, nested_set_options(Category, @category) {|i|
|
21
|
+
# "#{'–' * i.level} #{i.name}"
|
22
|
+
# }) %>
|
23
|
+
#
|
24
|
+
def nested_set_options(class_or_item, mover = nil)
|
25
|
+
if class_or_item.is_a? Array
|
26
|
+
items = class_or_item.reject { |e| !e.root? }
|
27
|
+
else
|
28
|
+
class_or_item = class_or_item.roots if class_or_item.respond_to?(:scoped)
|
29
|
+
items = Array(class_or_item)
|
30
|
+
end
|
31
|
+
result = []
|
32
|
+
items.each do |root|
|
33
|
+
result += root.class.associate_parents(root.self_and_descendants).map do |i|
|
34
|
+
if mover.nil? || mover.new_record? || mover.move_possible?(i)
|
35
|
+
[yield(i), i.id]
|
36
|
+
end
|
37
|
+
end.compact
|
38
|
+
end
|
39
|
+
result
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
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
|
@@ -0,0 +1,58 @@
|
|
1
|
+
module CollectiveIdea #:nodoc:
|
2
|
+
module Acts #:nodoc:
|
3
|
+
module NestedSet #:nodoc:
|
4
|
+
module Model
|
5
|
+
module Prunable
|
6
|
+
|
7
|
+
# Prunes a branch off of the tree, shifting all of the elements on the right
|
8
|
+
# back to the left so the counts still work.
|
9
|
+
def destroy_descendants
|
10
|
+
return if right.nil? || left.nil? || skip_before_destroy
|
11
|
+
|
12
|
+
in_tenacious_transaction do
|
13
|
+
reload_nested_set
|
14
|
+
# select the rows in the model that extend past the deletion point and apply a lock
|
15
|
+
nested_set_scope.right_of(left).select(id).lock(true)
|
16
|
+
|
17
|
+
destroy_or_delete_descendants
|
18
|
+
|
19
|
+
# update lefts and rights for remaining nodes
|
20
|
+
update_siblings_for_remaining_nodes
|
21
|
+
|
22
|
+
# Don't allow multiple calls to destroy to corrupt the set
|
23
|
+
self.skip_before_destroy = true
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def destroy_or_delete_descendants
|
28
|
+
if acts_as_nested_set_options[:dependent] == :destroy
|
29
|
+
descendants.each do |model|
|
30
|
+
model.skip_before_destroy = true
|
31
|
+
model.destroy
|
32
|
+
end
|
33
|
+
else
|
34
|
+
descendants.delete_all
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def update_siblings_for_remaining_nodes
|
39
|
+
update_siblings(:left)
|
40
|
+
update_siblings(:right)
|
41
|
+
end
|
42
|
+
|
43
|
+
def update_siblings(direction)
|
44
|
+
full_column_name = send("quoted_#{direction}_column_full_name")
|
45
|
+
column_name = send("quoted_#{direction}_column_name")
|
46
|
+
|
47
|
+
nested_set_scope.where(["#{full_column_name} > ?", right]).
|
48
|
+
update_all(["#{column_name} = (#{column_name} - ?)", diff])
|
49
|
+
end
|
50
|
+
|
51
|
+
def diff
|
52
|
+
right - left + 1
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|