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
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 559dfc3c4e84219413936644a4f9562bb5b99f7e
|
4
|
+
data.tar.gz: 94ab72b3cc301118a120e63a147dc002664c5ca3
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 75ca6ea8548d4099e7879832d4b66035dcd825adf027eb05f662ea3651bf63e32957f5e9a1aa5a98d47a1a889d697ed10c63ef0dc943bc0f8908c43279921da3
|
7
|
+
data.tar.gz: 3b678935e580e09010e66cc45114e01d103bb4b4aa726bef1885f969d1c54fd05d9bafc67b28db3af677f81e0749439de2dfa3b765d4785d453272045d3c10a3
|
data/README.md
ADDED
@@ -0,0 +1,163 @@
|
|
1
|
+
# AwesomeNestedSet
|
2
|
+
|
3
|
+
Awesome Nested Set is an implementation of the nested set pattern for ActiveRecord models.
|
4
|
+
It is a replacement for acts_as_nested_set and BetterNestedSet, but more awesome.
|
5
|
+
|
6
|
+
Version 2 supports Rails 3. Gem versions prior to 2.0 support Rails 2.
|
7
|
+
|
8
|
+
## What makes this so awesome?
|
9
|
+
|
10
|
+
This is a new implementation of nested set based off of BetterNestedSet that fixes some bugs, removes tons of duplication, adds a few useful methods, and adds STI support.
|
11
|
+
|
12
|
+
[](https://codeclimate.com/github/collectiveidea/awesome_nested_set)
|
13
|
+
|
14
|
+
## Installation
|
15
|
+
|
16
|
+
Add to your Gemfile:
|
17
|
+
|
18
|
+
```ruby
|
19
|
+
gem 'awesome_nested_set'
|
20
|
+
```
|
21
|
+
|
22
|
+
## Usage
|
23
|
+
|
24
|
+
To make use of `awesome_nested_set`, your model needs to have 3 fields:
|
25
|
+
`lft`, `rgt`, and `parent_id`. The names of these fields are configurable.
|
26
|
+
You can also have an optional field, `depth`:
|
27
|
+
|
28
|
+
```ruby
|
29
|
+
class CreateCategories < ActiveRecord::Migration
|
30
|
+
def self.up
|
31
|
+
create_table :categories do |t|
|
32
|
+
t.string :name
|
33
|
+
t.integer :parent_id
|
34
|
+
t.integer :lft
|
35
|
+
t.integer :rgt
|
36
|
+
t.integer :depth # this is optional.
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def self.down
|
41
|
+
drop_table :categories
|
42
|
+
end
|
43
|
+
end
|
44
|
+
```
|
45
|
+
|
46
|
+
Enable the nested set functionality by declaring `acts_as_nested_set` on your model
|
47
|
+
|
48
|
+
```ruby
|
49
|
+
class Category < ActiveRecord::Base
|
50
|
+
acts_as_nested_set
|
51
|
+
end
|
52
|
+
```
|
53
|
+
|
54
|
+
Run `rake rdoc` to generate the API docs and see [CollectiveIdea::Acts::NestedSet](lib/awesome_nested_set/awesome_nested_set.rb) for more information.
|
55
|
+
|
56
|
+
## Callbacks
|
57
|
+
|
58
|
+
There are three callbacks called when moving a node:
|
59
|
+
`before_move`, `after_move` and `around_move`.
|
60
|
+
|
61
|
+
```ruby
|
62
|
+
class Category < ActiveRecord::Base
|
63
|
+
acts_as_nested_set
|
64
|
+
|
65
|
+
after_move :rebuild_slug
|
66
|
+
around_move :da_fancy_things_around
|
67
|
+
|
68
|
+
private
|
69
|
+
|
70
|
+
def rebuild_slug
|
71
|
+
# do whatever
|
72
|
+
end
|
73
|
+
|
74
|
+
def da_fancy_things_around
|
75
|
+
# do something...
|
76
|
+
yield # actually moves
|
77
|
+
# do something else...
|
78
|
+
end
|
79
|
+
end
|
80
|
+
```
|
81
|
+
|
82
|
+
Beside this there are also hooks to act on the newly added or removed children.
|
83
|
+
|
84
|
+
```ruby
|
85
|
+
class Category < ActiveRecord::Base
|
86
|
+
acts_as_nested_set :before_add => :do_before_add_stuff,
|
87
|
+
:after_add => :do_after_add_stuff,
|
88
|
+
:before_remove => :do_before_remove_stuff,
|
89
|
+
:after_remove => :do_after_remove_stuff
|
90
|
+
|
91
|
+
private
|
92
|
+
|
93
|
+
def do_before_add_stuff(child_node)
|
94
|
+
# do whatever with the child
|
95
|
+
end
|
96
|
+
|
97
|
+
def do_after_add_stuff(child_node)
|
98
|
+
# do whatever with the child
|
99
|
+
end
|
100
|
+
|
101
|
+
def do_before_remove_stuff(child_node)
|
102
|
+
# do whatever with the child
|
103
|
+
end
|
104
|
+
|
105
|
+
def do_after_remove_stuff(child_node)
|
106
|
+
# do whatever with the child
|
107
|
+
end
|
108
|
+
end
|
109
|
+
```
|
110
|
+
|
111
|
+
## Protecting attributes from mass assignment
|
112
|
+
|
113
|
+
It's generally best to "whitelist" the attributes that can be used in mass assignment:
|
114
|
+
|
115
|
+
```ruby
|
116
|
+
class Category < ActiveRecord::Base
|
117
|
+
acts_as_nested_set
|
118
|
+
attr_accessible :name, :parent_id
|
119
|
+
end
|
120
|
+
```
|
121
|
+
|
122
|
+
If for some reason that is not possible, you will probably want to protect the `lft` and `rgt` attributes:
|
123
|
+
|
124
|
+
```ruby
|
125
|
+
class Category < ActiveRecord::Base
|
126
|
+
acts_as_nested_set
|
127
|
+
attr_protected :lft, :rgt
|
128
|
+
end
|
129
|
+
```
|
130
|
+
|
131
|
+
## Conversion from other trees
|
132
|
+
|
133
|
+
Coming from acts_as_tree or another system where you only have a parent_id? No problem. Simply add the lft & rgt fields as above, and then run:
|
134
|
+
|
135
|
+
```ruby
|
136
|
+
Category.rebuild!
|
137
|
+
```
|
138
|
+
|
139
|
+
Your tree will be converted to a valid nested set. Awesome!
|
140
|
+
|
141
|
+
## View Helper
|
142
|
+
|
143
|
+
The view helper is called #nested_set_options.
|
144
|
+
|
145
|
+
Example usage:
|
146
|
+
|
147
|
+
```erb
|
148
|
+
<%= f.select :parent_id, nested_set_options(Category, @category) {|i| "#{'-' * i.level} #{i.name}" } %>
|
149
|
+
|
150
|
+
<%= select_tag 'parent_id', options_for_select(nested_set_options(Category) {|i| "#{'-' * i.level} #{i.name}" } ) %>
|
151
|
+
```
|
152
|
+
|
153
|
+
See [CollectiveIdea::Acts::NestedSet::Helper](lib/awesome_nested_set/helper.rb) for more information about the helpers.
|
154
|
+
|
155
|
+
## References
|
156
|
+
|
157
|
+
You can learn more about nested sets at: http://threebit.net/tutorials/nestedset/tutorial1.html
|
158
|
+
|
159
|
+
## How to contribute
|
160
|
+
|
161
|
+
Please see the ['Contributing' document](CONTRIBUTING.md).
|
162
|
+
|
163
|
+
Copyright © 2008 - 2013 Collective Idea, released under the MIT license
|
data/lib/awesome_nested_set.rb
CHANGED
@@ -1,3 +1,6 @@
|
|
1
|
+
require 'awesome_nested_set/columns'
|
2
|
+
require 'awesome_nested_set/model'
|
3
|
+
|
1
4
|
module CollectiveIdea #:nodoc:
|
2
5
|
module Acts #:nodoc:
|
3
6
|
module NestedSet #:nodoc:
|
@@ -42,726 +45,90 @@ module CollectiveIdea #:nodoc:
|
|
42
45
|
# CollectiveIdea::Acts::NestedSet::Model for a list of instance methods added
|
43
46
|
# to acts_as_nested_set models
|
44
47
|
def acts_as_nested_set(options = {})
|
45
|
-
options
|
46
|
-
:parent_column => 'parent_id',
|
47
|
-
:left_column => 'lft',
|
48
|
-
:right_column => 'rgt',
|
49
|
-
:depth_column => 'depth',
|
50
|
-
:dependent => :delete_all, # or :destroy
|
51
|
-
:polymorphic => false,
|
52
|
-
:counter_cache => false
|
53
|
-
}.merge(options)
|
54
|
-
|
55
|
-
if options[:scope].is_a?(Symbol) && options[:scope].to_s !~ /_id$/
|
56
|
-
options[:scope] = "#{options[:scope]}_id".intern
|
57
|
-
end
|
58
|
-
|
59
|
-
class_attribute :acts_as_nested_set_options
|
60
|
-
self.acts_as_nested_set_options = options
|
48
|
+
acts_as_nested_set_parse_options! options
|
61
49
|
|
62
|
-
include
|
50
|
+
include Model
|
63
51
|
include Columns
|
64
52
|
extend Columns
|
65
53
|
|
66
|
-
|
67
|
-
|
68
|
-
:counter_cache => options[:counter_cache],
|
69
|
-
:inverse_of => (:children unless options[:polymorphic]),
|
70
|
-
:polymorphic => options[:polymorphic]
|
71
|
-
|
72
|
-
has_many_children_options = {
|
73
|
-
:class_name => self.base_class.to_s,
|
74
|
-
:foreign_key => parent_column_name,
|
75
|
-
:order => order_column,
|
76
|
-
:inverse_of => (:parent unless options[:polymorphic]),
|
77
|
-
}
|
78
|
-
|
79
|
-
# Add callbacks, if they were supplied.. otherwise, we don't want them.
|
80
|
-
[:before_add, :after_add, :before_remove, :after_remove].each do |ar_callback|
|
81
|
-
has_many_children_options.update(ar_callback => options[ar_callback]) if options[ar_callback]
|
82
|
-
end
|
83
|
-
|
84
|
-
has_many :children, has_many_children_options
|
54
|
+
acts_as_nested_set_relate_parent!
|
55
|
+
acts_as_nested_set_relate_children!
|
85
56
|
|
86
57
|
attr_accessor :skip_before_destroy
|
87
58
|
|
59
|
+
acts_as_nested_set_prevent_assignment_to_reserved_columns!
|
60
|
+
acts_as_nested_set_define_callbacks!
|
61
|
+
end
|
62
|
+
|
63
|
+
private
|
64
|
+
def acts_as_nested_set_define_callbacks!
|
65
|
+
# on creation, set automatically lft and rgt to the end of the tree
|
88
66
|
before_create :set_default_left_and_right
|
89
67
|
before_save :store_new_parent
|
90
68
|
after_save :move_to_new_parent, :set_depth!
|
91
69
|
before_destroy :destroy_descendants
|
92
70
|
|
93
|
-
# no assignment to structure fields
|
94
|
-
[left_column_name, right_column_name, depth_column_name].each do |column|
|
95
|
-
module_eval <<-"end_eval", __FILE__, __LINE__
|
96
|
-
def #{column}=(x)
|
97
|
-
raise ActiveRecord::ActiveRecordError, "Unauthorized assignment to #{column}: it's an internal field handled by acts_as_nested_set code, use move_to_* methods instead."
|
98
|
-
end
|
99
|
-
end_eval
|
100
|
-
end
|
101
|
-
|
102
71
|
define_model_callbacks :move
|
103
72
|
end
|
104
73
|
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
module ClassMethods
|
113
|
-
# Returns the first root
|
114
|
-
def root
|
115
|
-
roots.first
|
116
|
-
end
|
117
|
-
|
118
|
-
def roots
|
119
|
-
where(parent_column_name => nil).order(quoted_left_column_full_name)
|
120
|
-
end
|
121
|
-
|
122
|
-
def leaves
|
123
|
-
where("#{quoted_right_column_full_name} - #{quoted_left_column_full_name} = 1").order(quoted_left_column_full_name)
|
124
|
-
end
|
125
|
-
|
126
|
-
def valid?
|
127
|
-
left_and_rights_valid? && no_duplicates_for_columns? && all_roots_valid?
|
128
|
-
end
|
129
|
-
|
130
|
-
def left_and_rights_valid?
|
131
|
-
## AS clause not supported in Oracle in FROM clause for aliasing table name
|
132
|
-
joins("LEFT OUTER JOIN #{quoted_table_name}" +
|
133
|
-
(connection.adapter_name.match(/Oracle/).nil? ? " AS " : " ") +
|
134
|
-
"parent ON " +
|
135
|
-
"#{quoted_parent_column_full_name} = parent.#{primary_key}").
|
136
|
-
where(
|
137
|
-
"#{quoted_left_column_full_name} IS NULL OR " +
|
138
|
-
"#{quoted_right_column_full_name} IS NULL OR " +
|
139
|
-
"#{quoted_left_column_full_name} >= " +
|
140
|
-
"#{quoted_right_column_full_name} OR " +
|
141
|
-
"(#{quoted_parent_column_full_name} IS NOT NULL AND " +
|
142
|
-
"(#{quoted_left_column_full_name} <= parent.#{quoted_left_column_name} OR " +
|
143
|
-
"#{quoted_right_column_full_name} >= parent.#{quoted_right_column_name}))"
|
144
|
-
).count == 0
|
145
|
-
end
|
146
|
-
|
147
|
-
def no_duplicates_for_columns?
|
148
|
-
scope_string = Array(acts_as_nested_set_options[:scope]).map do |c|
|
149
|
-
connection.quote_column_name(c)
|
150
|
-
end.push(nil).join(", ")
|
151
|
-
[quoted_left_column_full_name, quoted_right_column_full_name].all? do |column|
|
152
|
-
# No duplicates
|
153
|
-
select("#{scope_string}#{column}, COUNT(#{column})").
|
154
|
-
group("#{scope_string}#{column}").
|
155
|
-
having("COUNT(#{column}) > 1").
|
156
|
-
first.nil?
|
157
|
-
end
|
158
|
-
end
|
159
|
-
|
160
|
-
# Wrapper for each_root_valid? that can deal with scope.
|
161
|
-
def all_roots_valid?
|
162
|
-
if acts_as_nested_set_options[:scope]
|
163
|
-
roots.group_by {|record| scope_column_names.collect {|col| record.send(col.to_sym) } }.all? do |scope, grouped_roots|
|
164
|
-
each_root_valid?(grouped_roots)
|
165
|
-
end
|
166
|
-
else
|
167
|
-
each_root_valid?(roots)
|
168
|
-
end
|
169
|
-
end
|
170
|
-
|
171
|
-
def each_root_valid?(roots_to_validate)
|
172
|
-
left = right = 0
|
173
|
-
roots_to_validate.all? do |root|
|
174
|
-
(root.left > left && root.right > right).tap do
|
175
|
-
left = root.left
|
176
|
-
right = root.right
|
177
|
-
end
|
178
|
-
end
|
179
|
-
end
|
180
|
-
|
181
|
-
# Rebuilds the left & rights if unset or invalid.
|
182
|
-
# Also very useful for converting from acts_as_tree.
|
183
|
-
def rebuild!(validate_nodes = true)
|
184
|
-
# default_scope with order may break database queries so we do all operation without scope
|
185
|
-
unscoped do
|
186
|
-
# Don't rebuild a valid tree.
|
187
|
-
return true if valid?
|
188
|
-
|
189
|
-
scope = lambda{|node|}
|
190
|
-
if acts_as_nested_set_options[:scope]
|
191
|
-
scope = lambda{|node|
|
192
|
-
scope_column_names.inject(""){|str, column_name|
|
193
|
-
str << "AND #{connection.quote_column_name(column_name)} = #{connection.quote(node.send(column_name.to_sym))} "
|
194
|
-
}
|
195
|
-
}
|
196
|
-
end
|
197
|
-
indices = {}
|
198
|
-
|
199
|
-
set_left_and_rights = lambda do |node|
|
200
|
-
# set left
|
201
|
-
node[left_column_name] = indices[scope.call(node)] += 1
|
202
|
-
# find
|
203
|
-
where(["#{quoted_parent_column_full_name} = ? #{scope.call(node)}", node]).order("#{quoted_left_column_full_name}, #{quoted_right_column_full_name}, id").each{|n| set_left_and_rights.call(n) }
|
204
|
-
# set right
|
205
|
-
node[right_column_name] = indices[scope.call(node)] += 1
|
206
|
-
node.save!(:validate => validate_nodes)
|
207
|
-
end
|
208
|
-
|
209
|
-
# Find root node(s)
|
210
|
-
root_nodes = where("#{quoted_parent_column_full_name} IS NULL").order("#{quoted_left_column_full_name}, #{quoted_right_column_full_name}, id").each do |root_node|
|
211
|
-
# setup index for this scope
|
212
|
-
indices[scope.call(root_node)] ||= 0
|
213
|
-
set_left_and_rights.call(root_node)
|
214
|
-
end
|
215
|
-
end
|
216
|
-
end
|
217
|
-
|
218
|
-
# Iterates over tree elements and determines the current level in the tree.
|
219
|
-
# Only accepts default ordering, odering by an other column than lft
|
220
|
-
# does not work. This method is much more efficent than calling level
|
221
|
-
# because it doesn't require any additional database queries.
|
222
|
-
#
|
223
|
-
# Example:
|
224
|
-
# Category.each_with_level(Category.root.self_and_descendants) do |o, level|
|
225
|
-
#
|
226
|
-
def each_with_level(objects)
|
227
|
-
path = [nil]
|
228
|
-
objects.each do |o|
|
229
|
-
if o.parent_id != path.last
|
230
|
-
# we are on a new level, did we descend or ascend?
|
231
|
-
if path.include?(o.parent_id)
|
232
|
-
# remove wrong wrong tailing paths elements
|
233
|
-
path.pop while path.last != o.parent_id
|
234
|
-
else
|
235
|
-
path << o.parent_id
|
236
|
-
end
|
237
|
-
end
|
238
|
-
yield(o, path.length - 1)
|
239
|
-
end
|
240
|
-
end
|
241
|
-
|
242
|
-
# Same as each_with_level - Accepts a string as a second argument to sort the list
|
243
|
-
# Example:
|
244
|
-
# Category.each_with_level(Category.root.self_and_descendants, :sort_by_this_column) do |o, level|
|
245
|
-
def sorted_each_with_level(objects, order)
|
246
|
-
path = [nil]
|
247
|
-
children = []
|
248
|
-
objects.each do |o|
|
249
|
-
children << o if o.leaf?
|
250
|
-
if o.parent_id != path.last
|
251
|
-
if !children.empty? && !o.leaf?
|
252
|
-
children.sort_by! &order
|
253
|
-
children.each { |c| yield(c, path.length-1) }
|
254
|
-
children = []
|
255
|
-
end
|
256
|
-
# we are on a new level, did we decent or ascent?
|
257
|
-
if path.include?(o.parent_id)
|
258
|
-
# remove wrong wrong tailing paths elements
|
259
|
-
path.pop while path.last != o.parent_id
|
260
|
-
else
|
261
|
-
path << o.parent_id
|
262
|
-
end
|
263
|
-
end
|
264
|
-
yield(o,path.length-1) if !o.leaf?
|
265
|
-
end
|
266
|
-
if !children.empty?
|
267
|
-
children.sort_by! &order
|
268
|
-
children.each { |c| yield(c, path.length-1) }
|
269
|
-
end
|
270
|
-
end
|
271
|
-
|
272
|
-
def associate_parents(objects)
|
273
|
-
if objects.all?{|o| o.respond_to?(:association)}
|
274
|
-
id_indexed = objects.index_by(&:id)
|
275
|
-
objects.each do |object|
|
276
|
-
if !(association = object.association(:parent)).loaded? && (parent = id_indexed[object.parent_id])
|
277
|
-
association.target = parent
|
278
|
-
association.set_inverse_instance(parent)
|
279
|
-
end
|
280
|
-
end
|
281
|
-
else
|
282
|
-
objects
|
283
|
-
end
|
284
|
-
end
|
285
|
-
end
|
286
|
-
|
287
|
-
# 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.
|
288
|
-
#
|
289
|
-
# category.self_and_descendants.count
|
290
|
-
# category.ancestors.find(:all, :conditions => "name like '%foo%'")
|
291
|
-
# Value of the parent column
|
292
|
-
def parent_id
|
293
|
-
self[parent_column_name]
|
294
|
-
end
|
295
|
-
|
296
|
-
# Value of the left column
|
297
|
-
def left
|
298
|
-
self[left_column_name]
|
299
|
-
end
|
300
|
-
|
301
|
-
# Value of the right column
|
302
|
-
def right
|
303
|
-
self[right_column_name]
|
304
|
-
end
|
305
|
-
|
306
|
-
# Returns true if this is a root node.
|
307
|
-
def root?
|
308
|
-
parent_id.nil?
|
309
|
-
end
|
310
|
-
|
311
|
-
# Returns true if this is the end of a branch.
|
312
|
-
def leaf?
|
313
|
-
persisted? && right.to_i - left.to_i == 1
|
314
|
-
end
|
315
|
-
|
316
|
-
# Returns true is this is a child node
|
317
|
-
def child?
|
318
|
-
!root?
|
319
|
-
end
|
320
|
-
|
321
|
-
# Returns root
|
322
|
-
def root
|
323
|
-
if persisted?
|
324
|
-
self_and_ancestors.where(parent_column_name => nil).first
|
325
|
-
else
|
326
|
-
if parent_id && current_parent = nested_set_scope.find(parent_id)
|
327
|
-
current_parent.root
|
328
|
-
else
|
329
|
-
self
|
330
|
-
end
|
331
|
-
end
|
332
|
-
end
|
333
|
-
|
334
|
-
# Returns the array of all parents and self
|
335
|
-
def self_and_ancestors
|
336
|
-
nested_set_scope.where([
|
337
|
-
"#{quoted_left_column_full_name} <= ? AND #{quoted_right_column_full_name} >= ?", left, right
|
338
|
-
])
|
339
|
-
end
|
340
|
-
|
341
|
-
# Returns an array of all parents
|
342
|
-
def ancestors
|
343
|
-
without_self self_and_ancestors
|
344
|
-
end
|
345
|
-
|
346
|
-
# Returns the array of all children of the parent, including self
|
347
|
-
def self_and_siblings
|
348
|
-
nested_set_scope.where(parent_column_name => parent_id)
|
349
|
-
end
|
350
|
-
|
351
|
-
# Returns the array of all children of the parent, except self
|
352
|
-
def siblings
|
353
|
-
without_self self_and_siblings
|
354
|
-
end
|
355
|
-
|
356
|
-
# Returns a set of all of its nested children which do not have children
|
357
|
-
def leaves
|
358
|
-
descendants.where("#{quoted_right_column_full_name} - #{quoted_left_column_full_name} = 1")
|
359
|
-
end
|
360
|
-
|
361
|
-
# Returns the level of this object in the tree
|
362
|
-
# root level is 0
|
363
|
-
def level
|
364
|
-
parent_id.nil? ? 0 : compute_level
|
365
|
-
end
|
366
|
-
|
367
|
-
# Returns a set of itself and all of its nested children
|
368
|
-
def self_and_descendants
|
369
|
-
nested_set_scope.where([
|
370
|
-
"#{quoted_left_column_full_name} >= ? AND #{quoted_left_column_full_name} < ?", left, right
|
371
|
-
# using _left_ for both sides here lets us benefit from an index on that column if one exists
|
372
|
-
])
|
373
|
-
end
|
374
|
-
|
375
|
-
# Returns a set of all of its children and nested children
|
376
|
-
def descendants
|
377
|
-
without_self self_and_descendants
|
378
|
-
end
|
379
|
-
|
380
|
-
def is_descendant_of?(other)
|
381
|
-
other.left < self.left && self.left < other.right && same_scope?(other)
|
382
|
-
end
|
383
|
-
|
384
|
-
def is_or_is_descendant_of?(other)
|
385
|
-
other.left <= self.left && self.left < other.right && same_scope?(other)
|
386
|
-
end
|
387
|
-
|
388
|
-
def is_ancestor_of?(other)
|
389
|
-
self.left < other.left && other.left < self.right && same_scope?(other)
|
390
|
-
end
|
391
|
-
|
392
|
-
def is_or_is_ancestor_of?(other)
|
393
|
-
self.left <= other.left && other.left < self.right && same_scope?(other)
|
394
|
-
end
|
395
|
-
|
396
|
-
# Check if other model is in the same scope
|
397
|
-
def same_scope?(other)
|
398
|
-
Array(acts_as_nested_set_options[:scope]).all? do |attr|
|
399
|
-
self.send(attr) == other.send(attr)
|
400
|
-
end
|
401
|
-
end
|
402
|
-
|
403
|
-
# Find the first sibling to the left
|
404
|
-
def left_sibling
|
405
|
-
siblings.where(["#{quoted_left_column_full_name} < ?", left]).
|
406
|
-
order("#{quoted_left_column_full_name} DESC").last
|
407
|
-
end
|
408
|
-
|
409
|
-
# Find the first sibling to the right
|
410
|
-
def right_sibling
|
411
|
-
siblings.where(["#{quoted_left_column_full_name} > ?", left]).first
|
412
|
-
end
|
413
|
-
|
414
|
-
# Shorthand method for finding the left sibling and moving to the left of it.
|
415
|
-
def move_left
|
416
|
-
move_to_left_of left_sibling
|
417
|
-
end
|
418
|
-
|
419
|
-
# Shorthand method for finding the right sibling and moving to the right of it.
|
420
|
-
def move_right
|
421
|
-
move_to_right_of right_sibling
|
422
|
-
end
|
423
|
-
|
424
|
-
# Move the node to the left of another node (you can pass id only)
|
425
|
-
def move_to_left_of(node)
|
426
|
-
move_to node, :left
|
427
|
-
end
|
428
|
-
|
429
|
-
# Move the node to the left of another node (you can pass id only)
|
430
|
-
def move_to_right_of(node)
|
431
|
-
move_to node, :right
|
432
|
-
end
|
433
|
-
|
434
|
-
# Move the node to the child of another node (you can pass id only)
|
435
|
-
def move_to_child_of(node)
|
436
|
-
move_to node, :child
|
437
|
-
end
|
438
|
-
|
439
|
-
# Move the node to the child of another node with specify index (you can pass id only)
|
440
|
-
def move_to_child_with_index(node, index)
|
441
|
-
if node.children.empty?
|
442
|
-
move_to_child_of(node)
|
443
|
-
elsif node.children.count == index
|
444
|
-
move_to_right_of(node.children.last)
|
445
|
-
else
|
446
|
-
move_to_left_of(node.children[index])
|
447
|
-
end
|
448
|
-
end
|
449
|
-
|
450
|
-
# Move the node to root nodes
|
451
|
-
def move_to_root
|
452
|
-
move_to nil, :root
|
453
|
-
end
|
454
|
-
|
455
|
-
# Order children in a nested set by an attribute
|
456
|
-
# Can order by any attribute class that uses the Comparable mixin, for example a string or integer
|
457
|
-
# Usage example when sorting categories alphabetically: @new_category.move_to_ordered_child_of(@root, "name")
|
458
|
-
def move_to_ordered_child_of(parent, order_attribute, ascending = true)
|
459
|
-
self.move_to_root and return unless parent
|
460
|
-
left = nil # This is needed, at least for the tests.
|
461
|
-
parent.children.each do |n| # Find the node immediately to the left of this node.
|
462
|
-
if ascending
|
463
|
-
left = n if n.send(order_attribute) < self.send(order_attribute)
|
464
|
-
else
|
465
|
-
left = n if n.send(order_attribute) > self.send(order_attribute)
|
466
|
-
end
|
467
|
-
end
|
468
|
-
self.move_to_child_of(parent)
|
469
|
-
return unless parent.children.count > 1 # Only need to order if there are multiple children.
|
470
|
-
if left # Self has a left neighbor.
|
471
|
-
self.move_to_right_of(left)
|
472
|
-
else # Self is the left most node.
|
473
|
-
self.move_to_left_of(parent.children[0])
|
474
|
-
end
|
475
|
-
end
|
476
|
-
|
477
|
-
def move_possible?(target)
|
478
|
-
self != target && # Can't target self
|
479
|
-
same_scope?(target) && # can't be in different scopes
|
480
|
-
# !(left..right).include?(target.left..target.right) # this needs tested more
|
481
|
-
# detect impossible move
|
482
|
-
!((left <= target.left && right >= target.left) or (left <= target.right && right >= target.right))
|
483
|
-
end
|
484
|
-
|
485
|
-
def to_text
|
486
|
-
self_and_descendants.map do |node|
|
487
|
-
"#{'*'*(node.level+1)} #{node.id} #{node.to_s} (#{node.parent_id}, #{node.left}, #{node.right})"
|
488
|
-
end.join("\n")
|
489
|
-
end
|
490
|
-
|
491
|
-
protected
|
492
|
-
def compute_level
|
493
|
-
node, nesting = self, 0
|
494
|
-
while (association = node.association(:parent)).loaded? && association.target
|
495
|
-
nesting += 1
|
496
|
-
node = node.parent
|
497
|
-
end if node.respond_to? :association
|
498
|
-
node == self ? ancestors.count : node.level + nesting
|
499
|
-
end
|
500
|
-
|
501
|
-
def without_self(scope)
|
502
|
-
scope.where(["#{self.class.quoted_table_name}.#{self.class.primary_key} != ?", self])
|
503
|
-
end
|
504
|
-
|
505
|
-
# All nested set queries should use this nested_set_scope, which performs finds on
|
506
|
-
# the base ActiveRecord class, using the :scope declared in the acts_as_nested_set
|
507
|
-
# declaration.
|
508
|
-
def nested_set_scope(options = {})
|
509
|
-
options = {:order => quoted_left_column_full_name}.merge(options)
|
510
|
-
scopes = Array(acts_as_nested_set_options[:scope])
|
511
|
-
options[:conditions] = scopes.inject({}) do |conditions,attr|
|
512
|
-
conditions.merge attr => self[attr]
|
513
|
-
end unless scopes.empty?
|
514
|
-
self.class.base_class.unscoped.scoped options
|
515
|
-
end
|
516
|
-
|
517
|
-
def store_new_parent
|
518
|
-
@move_to_new_parent_id = send("#{parent_column_name}_changed?") ? parent_id : false
|
519
|
-
true # force callback to return true
|
520
|
-
end
|
521
|
-
|
522
|
-
def move_to_new_parent
|
523
|
-
if @move_to_new_parent_id.nil?
|
524
|
-
move_to_root
|
525
|
-
elsif @move_to_new_parent_id
|
526
|
-
move_to_child_of(@move_to_new_parent_id)
|
527
|
-
end
|
528
|
-
end
|
529
|
-
|
530
|
-
def set_depth!
|
531
|
-
if nested_set_scope.column_names.map(&:to_s).include?(depth_column_name.to_s)
|
532
|
-
in_tenacious_transaction do
|
533
|
-
reload
|
534
|
-
|
535
|
-
nested_set_scope.where(:id => id).update_all(["#{quoted_depth_column_name} = ?", level])
|
536
|
-
end
|
537
|
-
self[depth_column_name.to_sym] = self.level
|
538
|
-
end
|
539
|
-
end
|
540
|
-
|
541
|
-
# on creation, set automatically lft and rgt to the end of the tree
|
542
|
-
def set_default_left_and_right
|
543
|
-
highest_right_row = nested_set_scope(:order => "#{quoted_right_column_full_name} desc").limit(1).lock(true).first
|
544
|
-
maxright = highest_right_row ? (highest_right_row[right_column_name] || 0) : 0
|
545
|
-
# adds the new node to the right of all existing nodes
|
546
|
-
self[left_column_name] = maxright + 1
|
547
|
-
self[right_column_name] = maxright + 2
|
548
|
-
end
|
549
|
-
|
550
|
-
def in_tenacious_transaction(&block)
|
551
|
-
retry_count = 0
|
552
|
-
begin
|
553
|
-
transaction(&block)
|
554
|
-
rescue ActiveRecord::StatementInvalid => error
|
555
|
-
raise unless connection.open_transactions.zero?
|
556
|
-
raise unless error.message =~ /Deadlock found when trying to get lock|Lock wait timeout exceeded/
|
557
|
-
raise unless retry_count < 10
|
558
|
-
retry_count += 1
|
559
|
-
logger.info "Deadlock detected on retry #{retry_count}, restarting transaction"
|
560
|
-
sleep(rand(retry_count)*0.1) # Aloha protocol
|
561
|
-
retry
|
562
|
-
end
|
563
|
-
end
|
564
|
-
|
565
|
-
# Prunes a branch off of the tree, shifting all of the elements on the right
|
566
|
-
# back to the left so the counts still work.
|
567
|
-
def destroy_descendants
|
568
|
-
return if right.nil? || left.nil? || skip_before_destroy
|
569
|
-
|
570
|
-
in_tenacious_transaction do
|
571
|
-
reload_nested_set
|
572
|
-
# select the rows in the model that extend past the deletion point and apply a lock
|
573
|
-
nested_set_scope.where(["#{quoted_left_column_full_name} >= ?", left]).
|
574
|
-
select(id).lock(true)
|
575
|
-
|
576
|
-
if acts_as_nested_set_options[:dependent] == :destroy
|
577
|
-
descendants.each do |model|
|
578
|
-
model.skip_before_destroy = true
|
579
|
-
model.destroy
|
580
|
-
end
|
581
|
-
else
|
582
|
-
nested_set_scope.where(["#{quoted_left_column_name} > ? AND #{quoted_right_column_name} < ?", left, right]).
|
583
|
-
delete_all
|
584
|
-
end
|
585
|
-
|
586
|
-
# update lefts and rights for remaining nodes
|
587
|
-
diff = right - left + 1
|
588
|
-
nested_set_scope.where(["#{quoted_left_column_full_name} > ?", right]).update_all(
|
589
|
-
["#{quoted_left_column_name} = (#{quoted_left_column_name} - ?)", diff]
|
590
|
-
)
|
591
|
-
|
592
|
-
nested_set_scope.where(["#{quoted_right_column_full_name} > ?", right]).update_all(
|
593
|
-
["#{quoted_right_column_name} = (#{quoted_right_column_name} - ?)", diff]
|
594
|
-
)
|
595
|
-
|
596
|
-
# Don't allow multiple calls to destroy to corrupt the set
|
597
|
-
self.skip_before_destroy = true
|
598
|
-
end
|
599
|
-
end
|
600
|
-
|
601
|
-
# reload left, right, and parent
|
602
|
-
def reload_nested_set
|
603
|
-
reload(
|
604
|
-
:select => "#{quoted_left_column_full_name}, #{quoted_right_column_full_name}, #{quoted_parent_column_full_name}",
|
605
|
-
:lock => true
|
606
|
-
)
|
607
|
-
end
|
608
|
-
|
609
|
-
def move_to(target, position)
|
610
|
-
raise ActiveRecord::ActiveRecordError, "You cannot move a new node" if self.new_record?
|
611
|
-
run_callbacks :move do
|
612
|
-
in_tenacious_transaction do
|
613
|
-
if target.is_a? self.class.base_class
|
614
|
-
target.reload_nested_set
|
615
|
-
elsif position != :root
|
616
|
-
# load object if node is not an object
|
617
|
-
target = nested_set_scope.find(target)
|
618
|
-
end
|
619
|
-
self.reload_nested_set
|
620
|
-
|
621
|
-
unless position == :root || move_possible?(target)
|
622
|
-
raise ActiveRecord::ActiveRecordError, "Impossible move, target node cannot be inside moved tree."
|
623
|
-
end
|
624
|
-
|
625
|
-
bound = case position
|
626
|
-
when :child; target[right_column_name]
|
627
|
-
when :left; target[left_column_name]
|
628
|
-
when :right; target[right_column_name] + 1
|
629
|
-
when :root; 1
|
630
|
-
else raise ActiveRecord::ActiveRecordError, "Position should be :child, :left, :right or :root ('#{position}' received)."
|
631
|
-
end
|
632
|
-
|
633
|
-
if bound > self[right_column_name]
|
634
|
-
bound = bound - 1
|
635
|
-
other_bound = self[right_column_name] + 1
|
636
|
-
else
|
637
|
-
other_bound = self[left_column_name] - 1
|
638
|
-
end
|
639
|
-
|
640
|
-
# there would be no change
|
641
|
-
return if bound == self[right_column_name] || bound == self[left_column_name]
|
642
|
-
|
643
|
-
# we have defined the boundaries of two non-overlapping intervals,
|
644
|
-
# so sorting puts both the intervals and their boundaries in order
|
645
|
-
a, b, c, d = [self[left_column_name], self[right_column_name], bound, other_bound].sort
|
646
|
-
|
647
|
-
# select the rows in the model between a and d, and apply a lock
|
648
|
-
self.class.base_class.select('id').lock(true).where(
|
649
|
-
["#{quoted_left_column_full_name} >= :a and #{quoted_right_column_full_name} <= :d", {:a => a, :d => d}]
|
650
|
-
)
|
651
|
-
|
652
|
-
new_parent = case position
|
653
|
-
when :child; target.id
|
654
|
-
when :root; nil
|
655
|
-
else target[parent_column_name]
|
656
|
-
end
|
657
|
-
|
658
|
-
where_statement = ["not (#{quoted_left_column_name} = CASE " +
|
659
|
-
"WHEN #{quoted_left_column_name} BETWEEN :a AND :b " +
|
660
|
-
"THEN #{quoted_left_column_name} + :d - :b " +
|
661
|
-
"WHEN #{quoted_left_column_name} BETWEEN :c AND :d " +
|
662
|
-
"THEN #{quoted_left_column_name} + :a - :c " +
|
663
|
-
"ELSE #{quoted_left_column_name} END AND " +
|
664
|
-
"#{quoted_right_column_name} = CASE " +
|
665
|
-
"WHEN #{quoted_right_column_name} BETWEEN :a AND :b " +
|
666
|
-
"THEN #{quoted_right_column_name} + :d - :b " +
|
667
|
-
"WHEN #{quoted_right_column_name} BETWEEN :c AND :d " +
|
668
|
-
"THEN #{quoted_right_column_name} + :a - :c " +
|
669
|
-
"ELSE #{quoted_right_column_name} END AND " +
|
670
|
-
"#{quoted_parent_column_name} = CASE " +
|
671
|
-
"WHEN #{self.class.base_class.primary_key} = :id THEN :new_parent " +
|
672
|
-
"ELSE #{quoted_parent_column_name} END)" ,
|
673
|
-
{:a => a, :b => b, :c => c, :d => d, :id => self.id, :new_parent => new_parent} ]
|
674
|
-
|
675
|
-
|
676
|
-
|
74
|
+
def acts_as_nested_set_relate_children!
|
75
|
+
has_many_children_options = {
|
76
|
+
:class_name => self.base_class.to_s,
|
77
|
+
:foreign_key => parent_column_name,
|
78
|
+
:order => quoted_order_column_name,
|
79
|
+
:inverse_of => (:parent unless acts_as_nested_set_options[:polymorphic]),
|
80
|
+
}
|
677
81
|
|
678
|
-
|
679
|
-
|
680
|
-
|
681
|
-
"THEN #{quoted_left_column_name} + :d - :b " +
|
682
|
-
"WHEN #{quoted_left_column_name} BETWEEN :c AND :d " +
|
683
|
-
"THEN #{quoted_left_column_name} + :a - :c " +
|
684
|
-
"ELSE #{quoted_left_column_name} END, " +
|
685
|
-
"#{quoted_right_column_name} = CASE " +
|
686
|
-
"WHEN #{quoted_right_column_name} BETWEEN :a AND :b " +
|
687
|
-
"THEN #{quoted_right_column_name} + :d - :b " +
|
688
|
-
"WHEN #{quoted_right_column_name} BETWEEN :c AND :d " +
|
689
|
-
"THEN #{quoted_right_column_name} + :a - :c " +
|
690
|
-
"ELSE #{quoted_right_column_name} END, " +
|
691
|
-
"#{quoted_parent_column_name} = CASE " +
|
692
|
-
"WHEN #{self.class.base_class.primary_key} = :id THEN :new_parent " +
|
693
|
-
"ELSE #{quoted_parent_column_name} END",
|
694
|
-
{:a => a, :b => b, :c => c, :d => d, :id => self.id, :new_parent => new_parent}
|
695
|
-
])
|
696
|
-
end
|
697
|
-
target.reload_nested_set if target
|
698
|
-
self.set_depth!
|
699
|
-
self.descendants.each(&:save)
|
700
|
-
self.reload_nested_set
|
701
|
-
end
|
82
|
+
# Add callbacks, if they were supplied.. otherwise, we don't want them.
|
83
|
+
[:before_add, :after_add, :before_remove, :after_remove].each do |ar_callback|
|
84
|
+
has_many_children_options.update(ar_callback => acts_as_nested_set_options[ar_callback]) if acts_as_nested_set_options[ar_callback]
|
702
85
|
end
|
703
86
|
|
87
|
+
order_condition = has_many_children_options.delete(:order)
|
88
|
+
has_many :children, -> { order(order_condition) }, has_many_children_options
|
704
89
|
end
|
705
90
|
|
706
|
-
|
707
|
-
|
708
|
-
|
709
|
-
|
710
|
-
|
711
|
-
|
712
|
-
|
713
|
-
acts_as_nested_set_options[:right_column]
|
714
|
-
end
|
715
|
-
|
716
|
-
def depth_column_name
|
717
|
-
acts_as_nested_set_options[:depth_column]
|
718
|
-
end
|
719
|
-
|
720
|
-
def parent_column_name
|
721
|
-
acts_as_nested_set_options[:parent_column]
|
722
|
-
end
|
723
|
-
|
724
|
-
def order_column
|
725
|
-
acts_as_nested_set_options[:order_column] || left_column_name
|
726
|
-
end
|
727
|
-
|
728
|
-
def scope_column_names
|
729
|
-
Array(acts_as_nested_set_options[:scope])
|
730
|
-
end
|
731
|
-
|
732
|
-
def quoted_left_column_name
|
733
|
-
connection.quote_column_name(left_column_name)
|
734
|
-
end
|
735
|
-
|
736
|
-
def quoted_right_column_name
|
737
|
-
connection.quote_column_name(right_column_name)
|
738
|
-
end
|
739
|
-
|
740
|
-
def quoted_depth_column_name
|
741
|
-
connection.quote_column_name(depth_column_name)
|
742
|
-
end
|
91
|
+
def acts_as_nested_set_relate_parent!
|
92
|
+
belongs_to :parent, :class_name => self.base_class.to_s,
|
93
|
+
:foreign_key => parent_column_name,
|
94
|
+
:counter_cache => acts_as_nested_set_options[:counter_cache],
|
95
|
+
:inverse_of => (:children unless acts_as_nested_set_options[:polymorphic]),
|
96
|
+
:polymorphic => acts_as_nested_set_options[:polymorphic]
|
97
|
+
end
|
743
98
|
|
744
|
-
|
745
|
-
|
746
|
-
|
99
|
+
def acts_as_nested_set_default_options
|
100
|
+
{
|
101
|
+
:parent_column => 'parent_id',
|
102
|
+
:left_column => 'lft',
|
103
|
+
:right_column => 'rgt',
|
104
|
+
:depth_column => 'depth',
|
105
|
+
:dependent => :delete_all, # or :destroy
|
106
|
+
:polymorphic => false,
|
107
|
+
:counter_cache => false
|
108
|
+
}.freeze
|
109
|
+
end
|
747
110
|
|
748
|
-
|
749
|
-
|
750
|
-
end
|
111
|
+
def acts_as_nested_set_parse_options!(options)
|
112
|
+
options = acts_as_nested_set_default_options.merge(options)
|
751
113
|
|
752
|
-
|
753
|
-
"#{
|
114
|
+
if options[:scope].is_a?(Symbol) && options[:scope].to_s !~ /_id$/
|
115
|
+
options[:scope] = "#{options[:scope]}_id".intern
|
754
116
|
end
|
755
117
|
|
756
|
-
|
757
|
-
|
758
|
-
|
118
|
+
class_attribute :acts_as_nested_set_options
|
119
|
+
self.acts_as_nested_set_options = options
|
120
|
+
end
|
759
121
|
|
760
|
-
|
761
|
-
|
122
|
+
def acts_as_nested_set_prevent_assignment_to_reserved_columns!
|
123
|
+
# no assignment to structure fields
|
124
|
+
[left_column_name, right_column_name, depth_column_name].each do |column|
|
125
|
+
module_eval <<-"end_eval", __FILE__, __LINE__
|
126
|
+
def #{column}=(x)
|
127
|
+
raise ActiveRecord::ActiveRecordError, "Unauthorized assignment to #{column}: it's an internal field handled by acts_as_nested_set code, use move_to_* methods instead."
|
128
|
+
end
|
129
|
+
end_eval
|
762
130
|
end
|
763
131
|
end
|
764
|
-
|
765
132
|
end
|
766
133
|
end
|
767
134
|
end
|