awesome_nested_set 2.1.4 → 2.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/CHANGELOG +9 -1
- data/README.rdoc +51 -0
- data/lib/awesome_nested_set/awesome_nested_set.rb +82 -35
- data/lib/awesome_nested_set/version.rb +2 -2
- metadata +19 -3
data/CHANGELOG
CHANGED
@@ -1,3 +1,11 @@
|
|
1
|
+
2.1.5
|
2
|
+
* Worked around issues where AR#association wasn't present on Rails 3.0.x. [Philip Arndt]
|
3
|
+
* Adds option 'order_column' which defaults to 'left_column_name'. [gudata]
|
4
|
+
* Added moving with order functionality. [Sytse Sijbrandij]
|
5
|
+
* Use tablename in all select queries. [Mikhail Dieterle]
|
6
|
+
* Made sure all descendants' depths are updated when moving parent, not just immediate child. [Phil Thompson]
|
7
|
+
* Add documentation of the callbacks. [Tobias Maier]
|
8
|
+
|
1
9
|
2.1.4
|
2
10
|
* nested_set_options accept both Class & AR Relation. [Semyon Perepelitsa]
|
3
11
|
* Reduce the number of queries triggered by the canonical usage of `i.level` in the `nested_set` helpers. [thedarkone]
|
@@ -41,4 +49,4 @@
|
|
41
49
|
* Expect Rails 3
|
42
50
|
* Changed how callbacks work. Returning false in a before_move action does not block save operations. Use a validation or exception in the callback if you need that.
|
43
51
|
* Switched to RSpec
|
44
|
-
* Remove use of Comparable
|
52
|
+
* Remove use of Comparable
|
data/README.rdoc
CHANGED
@@ -43,6 +43,57 @@ Enable the nested set functionality by declaring acts_as_nested_set on your mode
|
|
43
43
|
|
44
44
|
Run `rake rdoc` to generate the API docs and see CollectiveIdea::Acts::NestedSet for more info.
|
45
45
|
|
46
|
+
== Callbacks
|
47
|
+
|
48
|
+
There are three callbacks called when moving a node. `before_move`, `after_move` and `around_move`.
|
49
|
+
|
50
|
+
class Category < ActiveRecord::Base
|
51
|
+
acts_as_nested_set
|
52
|
+
|
53
|
+
after_move :rebuild_slug
|
54
|
+
around_move :da_fancy_things_around
|
55
|
+
|
56
|
+
private
|
57
|
+
|
58
|
+
def rebuild_slug
|
59
|
+
# do whatever
|
60
|
+
end
|
61
|
+
|
62
|
+
def da_fancy_things_around
|
63
|
+
# do something...
|
64
|
+
yield # actually moves
|
65
|
+
# do something else...
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
Beside this there are also hooks to act on the newly added or removed children.
|
70
|
+
|
71
|
+
class Category < ActiveRecord::Base
|
72
|
+
acts_as_nested_set :before_add => :do_before_add_stuff,
|
73
|
+
:after_add => :do_after_add_stuff,
|
74
|
+
:before_remove => :do_before_remove_stuff,
|
75
|
+
:after_remove => :do_after_remove_stuff
|
76
|
+
|
77
|
+
private
|
78
|
+
|
79
|
+
def do_before_add_stuff(child_node)
|
80
|
+
# do whatever with the child
|
81
|
+
end
|
82
|
+
|
83
|
+
def do_after_add_stuff(child_node)
|
84
|
+
# do whatever with the child
|
85
|
+
end
|
86
|
+
|
87
|
+
def do_before_remove_stuff(child_node)
|
88
|
+
# do whatever with the child
|
89
|
+
end
|
90
|
+
|
91
|
+
def do_after_remove_stuff(child_node)
|
92
|
+
# do whatever with the child
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
|
46
97
|
== Protecting attributes from mass assignment
|
47
98
|
|
48
99
|
It's generally best to "white list" the attributes that can be used in mass assignment:
|
@@ -35,6 +35,8 @@ module CollectiveIdea #:nodoc:
|
|
35
35
|
# * +:counter_cache+ adds a counter cache for the number of children.
|
36
36
|
# defaults to false.
|
37
37
|
# Example: <tt>acts_as_nested_set :counter_cache => :children_count</tt>
|
38
|
+
# * +:order_column+ on which column to do sorting, by default it is the left_column_name
|
39
|
+
# Example: <tt>acts_as_nested_set :order_column => :position</tt>
|
38
40
|
#
|
39
41
|
# See CollectiveIdea::Acts::NestedSet::Model::ClassMethods for a list of class methods and
|
40
42
|
# CollectiveIdea::Acts::NestedSet::Model for a list of instance methods added
|
@@ -70,7 +72,7 @@ module CollectiveIdea #:nodoc:
|
|
70
72
|
has_many_children_options = {
|
71
73
|
:class_name => self.base_class.to_s,
|
72
74
|
:foreign_key => parent_column_name,
|
73
|
-
:order =>
|
75
|
+
:order => order_column,
|
74
76
|
:inverse_of => (:parent unless options[:polymorphic]),
|
75
77
|
}
|
76
78
|
|
@@ -103,6 +105,10 @@ module CollectiveIdea #:nodoc:
|
|
103
105
|
module Model
|
104
106
|
extend ActiveSupport::Concern
|
105
107
|
|
108
|
+
included do
|
109
|
+
delegate :quoted_table_name, :to => self
|
110
|
+
end
|
111
|
+
|
106
112
|
module ClassMethods
|
107
113
|
# Returns the first root
|
108
114
|
def root
|
@@ -110,11 +116,11 @@ module CollectiveIdea #:nodoc:
|
|
110
116
|
end
|
111
117
|
|
112
118
|
def roots
|
113
|
-
where(parent_column_name => nil).order(
|
119
|
+
where(parent_column_name => nil).order(quoted_left_column_full_name)
|
114
120
|
end
|
115
121
|
|
116
122
|
def leaves
|
117
|
-
where("#{
|
123
|
+
where("#{quoted_right_column_full_name} - #{quoted_left_column_full_name} = 1").order(quoted_left_column_full_name)
|
118
124
|
end
|
119
125
|
|
120
126
|
def valid?
|
@@ -126,15 +132,15 @@ module CollectiveIdea #:nodoc:
|
|
126
132
|
joins("LEFT OUTER JOIN #{quoted_table_name}" +
|
127
133
|
(connection.adapter_name.match(/Oracle/).nil? ? " AS " : " ") +
|
128
134
|
"parent ON " +
|
129
|
-
"#{
|
135
|
+
"#{quoted_parent_column_full_name} = parent.#{primary_key}").
|
130
136
|
where(
|
131
|
-
"#{
|
132
|
-
"#{
|
133
|
-
"#{
|
134
|
-
"#{
|
135
|
-
"(#{
|
136
|
-
"(#{
|
137
|
-
"#{
|
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}))"
|
138
144
|
).count == 0
|
139
145
|
end
|
140
146
|
|
@@ -142,7 +148,7 @@ module CollectiveIdea #:nodoc:
|
|
142
148
|
scope_string = Array(acts_as_nested_set_options[:scope]).map do |c|
|
143
149
|
connection.quote_column_name(c)
|
144
150
|
end.push(nil).join(", ")
|
145
|
-
[
|
151
|
+
[quoted_left_column_full_name, quoted_right_column_full_name].all? do |column|
|
146
152
|
# No duplicates
|
147
153
|
select("#{scope_string}#{column}, COUNT(#{column})").
|
148
154
|
group("#{scope_string}#{column}").
|
@@ -192,14 +198,14 @@ module CollectiveIdea #:nodoc:
|
|
192
198
|
# set left
|
193
199
|
node[left_column_name] = indices[scope.call(node)] += 1
|
194
200
|
# find
|
195
|
-
where(["#{
|
201
|
+
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) }
|
196
202
|
# set right
|
197
203
|
node[right_column_name] = indices[scope.call(node)] += 1
|
198
204
|
node.save!(:validate => validate_nodes)
|
199
205
|
end
|
200
206
|
|
201
207
|
# Find root node(s)
|
202
|
-
root_nodes = where("#{
|
208
|
+
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|
|
203
209
|
# setup index for this scope
|
204
210
|
indices[scope.call(root_node)] ||= 0
|
205
211
|
set_left_and_rights.call(root_node)
|
@@ -261,12 +267,16 @@ module CollectiveIdea #:nodoc:
|
|
261
267
|
end
|
262
268
|
|
263
269
|
def associate_parents(objects)
|
264
|
-
|
265
|
-
|
266
|
-
|
267
|
-
association.
|
268
|
-
|
270
|
+
if objects.all?{|o| o.respond_to?(:association)}
|
271
|
+
id_indexed = objects.index_by(&:id)
|
272
|
+
objects.each do |object|
|
273
|
+
if !(association = object.association(:parent)).loaded? && (parent = id_indexed[object.parent_id])
|
274
|
+
association.target = parent
|
275
|
+
association.set_inverse_instance(parent)
|
276
|
+
end
|
269
277
|
end
|
278
|
+
else
|
279
|
+
objects
|
270
280
|
end
|
271
281
|
end
|
272
282
|
end
|
@@ -321,7 +331,7 @@ module CollectiveIdea #:nodoc:
|
|
321
331
|
# Returns the array of all parents and self
|
322
332
|
def self_and_ancestors
|
323
333
|
nested_set_scope.where([
|
324
|
-
"#{
|
334
|
+
"#{quoted_left_column_full_name} <= ? AND #{quoted_right_column_full_name} >= ?", left, right
|
325
335
|
])
|
326
336
|
end
|
327
337
|
|
@@ -342,7 +352,7 @@ module CollectiveIdea #:nodoc:
|
|
342
352
|
|
343
353
|
# Returns a set of all of its nested children which do not have children
|
344
354
|
def leaves
|
345
|
-
descendants.where("#{
|
355
|
+
descendants.where("#{quoted_right_column_full_name} - #{quoted_left_column_full_name} = 1")
|
346
356
|
end
|
347
357
|
|
348
358
|
# Returns the level of this object in the tree
|
@@ -354,7 +364,7 @@ module CollectiveIdea #:nodoc:
|
|
354
364
|
# Returns a set of itself and all of its nested children
|
355
365
|
def self_and_descendants
|
356
366
|
nested_set_scope.where([
|
357
|
-
"#{
|
367
|
+
"#{quoted_left_column_full_name} >= ? AND #{quoted_left_column_full_name} < ?", left, right
|
358
368
|
# using _left_ for both sides here lets us benefit from an index on that column if one exists
|
359
369
|
])
|
360
370
|
end
|
@@ -389,13 +399,13 @@ module CollectiveIdea #:nodoc:
|
|
389
399
|
|
390
400
|
# Find the first sibling to the left
|
391
401
|
def left_sibling
|
392
|
-
siblings.where(["#{
|
393
|
-
order("#{
|
402
|
+
siblings.where(["#{quoted_left_column_full_name} < ?", left]).
|
403
|
+
order("#{quoted_left_column_full_name} DESC").last
|
394
404
|
end
|
395
405
|
|
396
406
|
# Find the first sibling to the right
|
397
407
|
def right_sibling
|
398
|
-
siblings.where(["#{
|
408
|
+
siblings.where(["#{quoted_left_column_full_name} > ?", left]).first
|
399
409
|
end
|
400
410
|
|
401
411
|
# Shorthand method for finding the left sibling and moving to the left of it.
|
@@ -439,6 +449,28 @@ module CollectiveIdea #:nodoc:
|
|
439
449
|
move_to nil, :root
|
440
450
|
end
|
441
451
|
|
452
|
+
# Order children in a nested set by an attribute
|
453
|
+
# Can order by any attribute class that uses the Comparable mixin, for example a string or integer
|
454
|
+
# Usage example when sorting categories alphabetically: @new_category.move_to_ordered_child_of(@root, "name")
|
455
|
+
def move_to_ordered_child_of(parent, order_attribute, ascending = true)
|
456
|
+
self.move_to_root and return unless parent
|
457
|
+
left = nil # This is needed, at least for the tests.
|
458
|
+
parent.children.each do |n| # Find the node immediately to the left of this node.
|
459
|
+
if ascending
|
460
|
+
left = n if n.send(order_attribute) < self.send(order_attribute)
|
461
|
+
else
|
462
|
+
left = n if n.send(order_attribute) > self.send(order_attribute)
|
463
|
+
end
|
464
|
+
end
|
465
|
+
self.move_to_child_of(parent)
|
466
|
+
return unless parent.children.count > 1 # Only need to order if there are multiple children.
|
467
|
+
if left # Self has a left neighbor.
|
468
|
+
self.move_to_right_of(left)
|
469
|
+
else # Self is the left most node.
|
470
|
+
self.move_to_left_of(parent.children[0])
|
471
|
+
end
|
472
|
+
end
|
473
|
+
|
442
474
|
def move_possible?(target)
|
443
475
|
self != target && # Can't target self
|
444
476
|
same_scope?(target) && # can't be in different scopes
|
@@ -459,7 +491,7 @@ module CollectiveIdea #:nodoc:
|
|
459
491
|
while (association = node.association(:parent)).loaded? && association.target
|
460
492
|
nesting += 1
|
461
493
|
node = node.parent
|
462
|
-
end
|
494
|
+
end if node.respond_to? :association
|
463
495
|
node == self ? ancestors.count : node.level + nesting
|
464
496
|
end
|
465
497
|
|
@@ -471,7 +503,7 @@ module CollectiveIdea #:nodoc:
|
|
471
503
|
# the base ActiveRecord class, using the :scope declared in the acts_as_nested_set
|
472
504
|
# declaration.
|
473
505
|
def nested_set_scope(options = {})
|
474
|
-
options = {:order =>
|
506
|
+
options = {:order => quoted_left_column_full_name}.merge(options)
|
475
507
|
scopes = Array(acts_as_nested_set_options[:scope])
|
476
508
|
options[:conditions] = scopes.inject({}) do |conditions,attr|
|
477
509
|
conditions.merge attr => self[attr]
|
@@ -505,7 +537,7 @@ module CollectiveIdea #:nodoc:
|
|
505
537
|
|
506
538
|
# on creation, set automatically lft and rgt to the end of the tree
|
507
539
|
def set_default_left_and_right
|
508
|
-
highest_right_row = nested_set_scope(:order => "#{
|
540
|
+
highest_right_row = nested_set_scope(:order => "#{quoted_right_column_full_name} desc").limit(1).lock(true).first
|
509
541
|
maxright = highest_right_row ? (highest_right_row[right_column_name] || 0) : 0
|
510
542
|
# adds the new node to the right of all existing nodes
|
511
543
|
self[left_column_name] = maxright + 1
|
@@ -535,7 +567,7 @@ module CollectiveIdea #:nodoc:
|
|
535
567
|
in_tenacious_transaction do
|
536
568
|
reload_nested_set
|
537
569
|
# select the rows in the model that extend past the deletion point and apply a lock
|
538
|
-
nested_set_scope.where(["#{
|
570
|
+
nested_set_scope.where(["#{quoted_left_column_full_name} >= ?", left]).
|
539
571
|
select(id).lock(true)
|
540
572
|
|
541
573
|
if acts_as_nested_set_options[:dependent] == :destroy
|
@@ -550,11 +582,11 @@ module CollectiveIdea #:nodoc:
|
|
550
582
|
|
551
583
|
# update lefts and rights for remaining nodes
|
552
584
|
diff = right - left + 1
|
553
|
-
nested_set_scope.where(["#{
|
585
|
+
nested_set_scope.where(["#{quoted_left_column_full_name} > ?", right]).update_all(
|
554
586
|
["#{quoted_left_column_name} = (#{quoted_left_column_name} - ?)", diff]
|
555
587
|
)
|
556
588
|
|
557
|
-
nested_set_scope.where(["#{
|
589
|
+
nested_set_scope.where(["#{quoted_right_column_full_name} > ?", right]).update_all(
|
558
590
|
["#{quoted_right_column_name} = (#{quoted_right_column_name} - ?)", diff]
|
559
591
|
)
|
560
592
|
|
@@ -566,7 +598,7 @@ module CollectiveIdea #:nodoc:
|
|
566
598
|
# reload left, right, and parent
|
567
599
|
def reload_nested_set
|
568
600
|
reload(
|
569
|
-
:select => "#{
|
601
|
+
:select => "#{quoted_left_column_full_name}, #{quoted_right_column_full_name}, #{quoted_parent_column_full_name}",
|
570
602
|
:lock => true
|
571
603
|
)
|
572
604
|
end
|
@@ -611,7 +643,7 @@ module CollectiveIdea #:nodoc:
|
|
611
643
|
|
612
644
|
# select the rows in the model between a and d, and apply a lock
|
613
645
|
self.class.base_class.select('id').lock(true).where(
|
614
|
-
["#{
|
646
|
+
["#{quoted_left_column_full_name} >= :a and #{quoted_right_column_full_name} <= :d", {:a => a, :d => d}]
|
615
647
|
)
|
616
648
|
|
617
649
|
new_parent = case position
|
@@ -641,7 +673,7 @@ module CollectiveIdea #:nodoc:
|
|
641
673
|
end
|
642
674
|
target.reload_nested_set if target
|
643
675
|
self.set_depth!
|
644
|
-
self.
|
676
|
+
self.descendants.each(&:save)
|
645
677
|
self.reload_nested_set
|
646
678
|
end
|
647
679
|
end
|
@@ -666,6 +698,10 @@ module CollectiveIdea #:nodoc:
|
|
666
698
|
acts_as_nested_set_options[:parent_column]
|
667
699
|
end
|
668
700
|
|
701
|
+
def order_column
|
702
|
+
acts_as_nested_set_options[:order_column] || left_column_name
|
703
|
+
end
|
704
|
+
|
669
705
|
def scope_column_names
|
670
706
|
Array(acts_as_nested_set_options[:scope])
|
671
707
|
end
|
@@ -689,9 +725,20 @@ module CollectiveIdea #:nodoc:
|
|
689
725
|
def quoted_scope_column_names
|
690
726
|
scope_column_names.collect {|column_name| connection.quote_column_name(column_name) }
|
691
727
|
end
|
728
|
+
|
729
|
+
def quoted_left_column_full_name
|
730
|
+
"#{quoted_table_name}.#{quoted_left_column_name}"
|
731
|
+
end
|
732
|
+
|
733
|
+
def quoted_right_column_full_name
|
734
|
+
"#{quoted_table_name}.#{quoted_right_column_name}"
|
735
|
+
end
|
736
|
+
|
737
|
+
def quoted_parent_column_full_name
|
738
|
+
"#{quoted_table_name}.#{quoted_parent_column_name}"
|
739
|
+
end
|
692
740
|
end
|
693
741
|
|
694
742
|
end
|
695
743
|
end
|
696
744
|
end
|
697
|
-
|
@@ -1,3 +1,3 @@
|
|
1
1
|
module AwesomeNestedSet
|
2
|
-
VERSION = '2.1.
|
3
|
-
end
|
2
|
+
VERSION = '2.1.5' unless defined?(::AwesomeNestedSet::VERSION)
|
3
|
+
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: awesome_nested_set
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 2.1.
|
4
|
+
version: 2.1.5
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -11,7 +11,7 @@ authors:
|
|
11
11
|
autorequire:
|
12
12
|
bindir: bin
|
13
13
|
cert_chain: []
|
14
|
-
date: 2012-
|
14
|
+
date: 2012-09-26 00:00:00.000000000 Z
|
15
15
|
dependencies:
|
16
16
|
- !ruby/object:Gem::Dependency
|
17
17
|
name: activerecord
|
@@ -29,6 +29,22 @@ dependencies:
|
|
29
29
|
- - ! '>='
|
30
30
|
- !ruby/object:Gem::Version
|
31
31
|
version: 3.0.0
|
32
|
+
- !ruby/object:Gem::Dependency
|
33
|
+
name: rspec-rails
|
34
|
+
requirement: !ruby/object:Gem::Requirement
|
35
|
+
none: false
|
36
|
+
requirements:
|
37
|
+
- - ~>
|
38
|
+
- !ruby/object:Gem::Version
|
39
|
+
version: '2.8'
|
40
|
+
type: :development
|
41
|
+
prerelease: false
|
42
|
+
version_requirements: !ruby/object:Gem::Requirement
|
43
|
+
none: false
|
44
|
+
requirements:
|
45
|
+
- - ~>
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '2.8'
|
32
48
|
description: An awesome nested set implementation for Active Record
|
33
49
|
email: info@collectiveidea.com
|
34
50
|
executables: []
|
@@ -67,7 +83,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
67
83
|
version: '0'
|
68
84
|
requirements: []
|
69
85
|
rubyforge_project:
|
70
|
-
rubygems_version: 1.8.
|
86
|
+
rubygems_version: 1.8.24
|
71
87
|
signing_key:
|
72
88
|
specification_version: 3
|
73
89
|
summary: An awesome nested set implementation for Active Record
|