awesome_nested_set 2.1.6 → 3.0.0.rc.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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