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