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.
@@ -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
@@ -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
+ [![Code Climate](https://codeclimate.com/github/collectiveidea/awesome_nested_set.png)](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
@@ -5,4 +5,4 @@ ActiveRecord::Base.send :extend, CollectiveIdea::Acts::NestedSet
5
5
  if defined?(ActionView)
6
6
  require 'awesome_nested_set/helper'
7
7
  ActionView::Base.send :include, CollectiveIdea::Acts::NestedSet::Helper
8
- end
8
+ end
@@ -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 CollectiveIdea::Acts::NestedSet::Model
50
+ include Model
63
51
  include Columns
64
52
  extend Columns
65
53
 
66
- belongs_to :parent, :class_name => self.base_class.to_s,
67
- :foreign_key => parent_column_name,
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
- module Model
106
- extend ActiveSupport::Concern
107
-
108
- included do
109
- delegate :quoted_table_name, :to => self
110
- end
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
- self.nested_set_scope.where(*where_statement).update_all([
679
- "#{quoted_left_column_name} = CASE " +
680
- "WHEN #{quoted_left_column_name} BETWEEN :a AND :b " +
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
- # Mixed into both classes and instances to provide easy access to the column names
707
- module Columns
708
- def left_column_name
709
- acts_as_nested_set_options[:left_column]
710
- end
711
-
712
- def right_column_name
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
- def quoted_parent_column_name
745
- connection.quote_column_name(parent_column_name)
746
- end
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
- def quoted_scope_column_names
749
- scope_column_names.collect {|column_name| connection.quote_column_name(column_name) }
750
- end
111
+ def acts_as_nested_set_parse_options!(options)
112
+ options = acts_as_nested_set_default_options.merge(options)
751
113
 
752
- def quoted_left_column_full_name
753
- "#{quoted_table_name}.#{quoted_left_column_name}"
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
- def quoted_right_column_full_name
757
- "#{quoted_table_name}.#{quoted_right_column_name}"
758
- end
118
+ class_attribute :acts_as_nested_set_options
119
+ self.acts_as_nested_set_options = options
120
+ end
759
121
 
760
- def quoted_parent_column_full_name
761
- "#{quoted_table_name}.#{quoted_parent_column_name}"
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