awesome_nested_set 2.0.1 → 2.1.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.
- data/CHANGELOG +24 -0
- data/README.rdoc +24 -6
- data/lib/awesome_nested_set/awesome_nested_set.rb +363 -270
- data/lib/awesome_nested_set/helper.rb +53 -4
- data/lib/awesome_nested_set/version.rb +2 -2
- metadata +20 -5
data/CHANGELOG
CHANGED
|
@@ -1,3 +1,27 @@
|
|
|
1
|
+
2.1.1
|
|
2
|
+
* Added 'depth' which indicates how many levels deep the node is.
|
|
3
|
+
This only works when you have a column called 'depth' in your table,
|
|
4
|
+
otherwise it doesn't set itself. [Philip Arndt]
|
|
5
|
+
* Rails 3.2 support added. [Gabriel Sobrinho]
|
|
6
|
+
* Oracle compatibility added. [Pikender Sharma]
|
|
7
|
+
* Adding row locking to deletion, locking source of pivot values, and adding retry on collisions. [Markus J. Q. Roberts]
|
|
8
|
+
* Added method and helper for sorting children by column. [bluegod]
|
|
9
|
+
* Fixed .all_roots_valid? to work with Postgres. [Joshua Clayton]
|
|
10
|
+
* Made compatible with polymorphic belongs_to. [Graham Randall]
|
|
11
|
+
* Added in the association callbacks to the children :has_many association. [Michael Deering]
|
|
12
|
+
* Modified helper to allow using array of objects as argument. [Rahmat Budiharso]
|
|
13
|
+
* Fixed cases where we were calling attr_protected. [Jacob Swanner]
|
|
14
|
+
* Fixed nil cases involving lft and rgt. [Stuart Coyle] and [Patrick Morgan]
|
|
15
|
+
|
|
16
|
+
2.0.2
|
|
17
|
+
* Fixed deprecation warning under Rails 3.1 [Philip Arndt]
|
|
18
|
+
* Converted Test::Unit matchers to RSpec. [Uģis Ozols]
|
|
19
|
+
* Added inverse_of to associations to improve performance rendering trees. [Sergio Cambra]
|
|
20
|
+
* Added row locking and fixed some race conditions. [Markus J. Q. Roberts]
|
|
21
|
+
|
|
22
|
+
2.0.1
|
|
23
|
+
* Fixed a bug with move_to not using nested_set_scope [Andreas Sekine]
|
|
24
|
+
|
|
1
25
|
2.0.0.pre
|
|
2
26
|
* Expect Rails 3
|
|
3
27
|
* 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.
|
data/README.rdoc
CHANGED
|
@@ -16,7 +16,8 @@ This is a new implementation of nested set based off of BetterNestedSet that fix
|
|
|
16
16
|
|
|
17
17
|
== Usage
|
|
18
18
|
|
|
19
|
-
To make use of awesome_nested_set, your model needs to have 3 fields: lft, rgt, and parent_id
|
|
19
|
+
To make use of awesome_nested_set, your model needs to have 3 fields: lft, rgt, and parent_id.
|
|
20
|
+
You can also have an optional field: depth:
|
|
20
21
|
|
|
21
22
|
class CreateCategories < ActiveRecord::Migration
|
|
22
23
|
def self.up
|
|
@@ -25,6 +26,7 @@ To make use of awesome_nested_set, your model needs to have 3 fields: lft, rgt,
|
|
|
25
26
|
t.integer :parent_id
|
|
26
27
|
t.integer :lft
|
|
27
28
|
t.integer :rgt
|
|
29
|
+
t.integer :depth # this is optional.
|
|
28
30
|
end
|
|
29
31
|
end
|
|
30
32
|
|
|
@@ -39,7 +41,23 @@ Enable the nested set functionality by declaring acts_as_nested_set on your mode
|
|
|
39
41
|
acts_as_nested_set
|
|
40
42
|
end
|
|
41
43
|
|
|
42
|
-
Run `rake rdoc` to generate the API docs and see CollectiveIdea::Acts::NestedSet
|
|
44
|
+
Run `rake rdoc` to generate the API docs and see CollectiveIdea::Acts::NestedSet for more info.
|
|
45
|
+
|
|
46
|
+
== Protecting attributes from mass assignment
|
|
47
|
+
|
|
48
|
+
It's generally best to "white list" the attributes that can be used in mass assignment:
|
|
49
|
+
|
|
50
|
+
class Category < ActiveRecord::Base
|
|
51
|
+
acts_as_nested_set
|
|
52
|
+
attr_accessible :name, :parent_id
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
If for some reason that is not possible, you will probably want to protect the lft and rgt attributes:
|
|
56
|
+
|
|
57
|
+
class Category < ActiveRecord::Base
|
|
58
|
+
acts_as_nested_set
|
|
59
|
+
attr_protected :lft, :rgt
|
|
60
|
+
end
|
|
43
61
|
|
|
44
62
|
== Conversion from other trees
|
|
45
63
|
|
|
@@ -70,15 +88,15 @@ You can learn more about nested sets at: http://threebit.net/tutorials/nestedset
|
|
|
70
88
|
If you find what you might think is a bug:
|
|
71
89
|
|
|
72
90
|
1. Check the GitHub issue tracker to see if anyone else has had the same issue.
|
|
73
|
-
|
|
91
|
+
https://github.com/collectiveidea/awesome_nested_set/issues/
|
|
74
92
|
2. If you don't see anything, create an issue with information on how to reproduce it.
|
|
75
93
|
|
|
76
94
|
If you want to contribute an enhancement or a fix:
|
|
77
95
|
|
|
78
|
-
1. Fork the project on
|
|
79
|
-
|
|
96
|
+
1. Fork the project on GitHub.
|
|
97
|
+
https://github.com/collectiveidea/awesome_nested_set/
|
|
80
98
|
2. Make your changes with tests.
|
|
81
99
|
3. Commit the changes without making changes to the Rakefile, VERSION, or any other files that aren't related to your enhancement or fix
|
|
82
100
|
4. Send a pull request.
|
|
83
101
|
|
|
84
|
-
Copyright ©2008 Collective Idea, released under the MIT license
|
|
102
|
+
Copyright ©2008 Collective Idea, released under the MIT license
|
|
@@ -23,6 +23,7 @@ module CollectiveIdea #:nodoc:
|
|
|
23
23
|
# * +:parent_column+ - specifies the column name to use for keeping the position integer (default: parent_id)
|
|
24
24
|
# * +:left_column+ - column name for left boundry data, default "lft"
|
|
25
25
|
# * +:right_column+ - column name for right boundry data, default "rgt"
|
|
26
|
+
# * +:depth_column+ - column name for the depth data, default "depth"
|
|
26
27
|
# * +:scope+ - restricts what is to be considered a list. Given a symbol, it'll attach "_id"
|
|
27
28
|
# (if it hasn't been already) and use that as the foreign key restriction. You
|
|
28
29
|
# can also pass an array to scope by multiple attributes.
|
|
@@ -36,14 +37,16 @@ module CollectiveIdea #:nodoc:
|
|
|
36
37
|
# Example: <tt>acts_as_nested_set :counter_cache => :children_count</tt>
|
|
37
38
|
#
|
|
38
39
|
# See CollectiveIdea::Acts::NestedSet::Model::ClassMethods for a list of class methods and
|
|
39
|
-
# CollectiveIdea::Acts::NestedSet::Model
|
|
40
|
+
# CollectiveIdea::Acts::NestedSet::Model for a list of instance methods added
|
|
40
41
|
# to acts_as_nested_set models
|
|
41
42
|
def acts_as_nested_set(options = {})
|
|
42
43
|
options = {
|
|
43
44
|
:parent_column => 'parent_id',
|
|
44
45
|
:left_column => 'lft',
|
|
45
46
|
:right_column => 'rgt',
|
|
47
|
+
:depth_column => 'depth',
|
|
46
48
|
:dependent => :delete_all, # or :destroy
|
|
49
|
+
:polymorphic => false,
|
|
47
50
|
:counter_cache => false
|
|
48
51
|
}.merge(options)
|
|
49
52
|
|
|
@@ -51,8 +54,8 @@ module CollectiveIdea #:nodoc:
|
|
|
51
54
|
options[:scope] = "#{options[:scope]}_id".intern
|
|
52
55
|
end
|
|
53
56
|
|
|
54
|
-
|
|
55
|
-
|
|
57
|
+
class_attribute :acts_as_nested_set_options
|
|
58
|
+
self.acts_as_nested_set_options = options
|
|
56
59
|
|
|
57
60
|
include CollectiveIdea::Acts::NestedSet::Model
|
|
58
61
|
include Columns
|
|
@@ -60,24 +63,26 @@ module CollectiveIdea #:nodoc:
|
|
|
60
63
|
|
|
61
64
|
belongs_to :parent, :class_name => self.base_class.to_s,
|
|
62
65
|
:foreign_key => parent_column_name,
|
|
63
|
-
:counter_cache => options[:counter_cache]
|
|
66
|
+
:counter_cache => options[:counter_cache],
|
|
67
|
+
:inverse_of => (:children unless options[:polymorphic]),
|
|
68
|
+
:polymorphic => options[:polymorphic]
|
|
64
69
|
has_many :children, :class_name => self.base_class.to_s,
|
|
65
|
-
:foreign_key => parent_column_name, :order =>
|
|
70
|
+
:foreign_key => parent_column_name, :order => left_column_name,
|
|
71
|
+
:inverse_of => (:parent unless options[:polymorphic]),
|
|
72
|
+
:before_add => options[:before_add],
|
|
73
|
+
:after_add => options[:after_add],
|
|
74
|
+
:before_remove => options[:before_remove],
|
|
75
|
+
:after_remove => options[:after_remove]
|
|
66
76
|
|
|
67
77
|
attr_accessor :skip_before_destroy
|
|
68
78
|
|
|
69
|
-
# no bulk assignment
|
|
70
|
-
if accessible_attributes.blank?
|
|
71
|
-
attr_protected left_column_name.intern, right_column_name.intern
|
|
72
|
-
end
|
|
73
|
-
|
|
74
79
|
before_create :set_default_left_and_right
|
|
75
80
|
before_save :store_new_parent
|
|
76
|
-
after_save :move_to_new_parent
|
|
81
|
+
after_save :move_to_new_parent, :set_depth!
|
|
77
82
|
before_destroy :destroy_descendants
|
|
78
83
|
|
|
79
84
|
# no assignment to structure fields
|
|
80
|
-
[left_column_name, right_column_name].each do |column|
|
|
85
|
+
[left_column_name, right_column_name, depth_column_name].each do |column|
|
|
81
86
|
module_eval <<-"end_eval", __FILE__, __LINE__
|
|
82
87
|
def #{column}=(x)
|
|
83
88
|
raise ActiveRecord::ActiveRecordError, "Unauthorized assignment to #{column}: it's an internal field handled by acts_as_nested_set code, use move_to_* methods instead."
|
|
@@ -85,10 +90,7 @@ module CollectiveIdea #:nodoc:
|
|
|
85
90
|
end_eval
|
|
86
91
|
end
|
|
87
92
|
|
|
88
|
-
|
|
89
|
-
scope :leaves, where("#{quoted_right_column_name} - #{quoted_left_column_name} = 1").order(quoted_left_column_name)
|
|
90
|
-
|
|
91
|
-
define_callbacks :move, :terminator => "result == false"
|
|
93
|
+
define_model_callbacks :move
|
|
92
94
|
end
|
|
93
95
|
|
|
94
96
|
module Model
|
|
@@ -100,12 +102,23 @@ module CollectiveIdea #:nodoc:
|
|
|
100
102
|
roots.first
|
|
101
103
|
end
|
|
102
104
|
|
|
105
|
+
def roots
|
|
106
|
+
where(parent_column_name => nil).order(quoted_left_column_name)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def leaves
|
|
110
|
+
where("#{quoted_right_column_name} - #{quoted_left_column_name} = 1").order(quoted_left_column_name)
|
|
111
|
+
end
|
|
112
|
+
|
|
103
113
|
def valid?
|
|
104
114
|
left_and_rights_valid? && no_duplicates_for_columns? && all_roots_valid?
|
|
105
115
|
end
|
|
106
116
|
|
|
107
|
-
def left_and_rights_valid?
|
|
108
|
-
|
|
117
|
+
def left_and_rights_valid?
|
|
118
|
+
## AS clause not supported in Oracle in FROM clause for aliasing table name
|
|
119
|
+
joins("LEFT OUTER JOIN #{quoted_table_name}" +
|
|
120
|
+
(connection.adapter_name.match(/Oracle/).nil? ? " AS " : " ") +
|
|
121
|
+
"parent ON " +
|
|
109
122
|
"#{quoted_table_name}.#{quoted_parent_column_name} = parent.#{primary_key}").
|
|
110
123
|
where(
|
|
111
124
|
"#{quoted_table_name}.#{quoted_left_column_name} IS NULL OR " +
|
|
@@ -134,7 +147,7 @@ module CollectiveIdea #:nodoc:
|
|
|
134
147
|
# Wrapper for each_root_valid? that can deal with scope.
|
|
135
148
|
def all_roots_valid?
|
|
136
149
|
if acts_as_nested_set_options[:scope]
|
|
137
|
-
roots.
|
|
150
|
+
roots.group_by {|record| scope_column_names.collect {|col| record.send(col.to_sym) } }.all? do |scope, grouped_roots|
|
|
138
151
|
each_root_valid?(grouped_roots)
|
|
139
152
|
end
|
|
140
153
|
else
|
|
@@ -198,7 +211,7 @@ module CollectiveIdea #:nodoc:
|
|
|
198
211
|
path = [nil]
|
|
199
212
|
objects.each do |o|
|
|
200
213
|
if o.parent_id != path.last
|
|
201
|
-
# we are on a new level, did we
|
|
214
|
+
# we are on a new level, did we descend or ascend?
|
|
202
215
|
if path.include?(o.parent_id)
|
|
203
216
|
# remove wrong wrong tailing paths elements
|
|
204
217
|
path.pop while path.last != o.parent_id
|
|
@@ -209,317 +222,388 @@ module CollectiveIdea #:nodoc:
|
|
|
209
222
|
yield(o, path.length - 1)
|
|
210
223
|
end
|
|
211
224
|
end
|
|
225
|
+
|
|
226
|
+
# Same as each_with_level - Accepts a string as a second argument to sort the list
|
|
227
|
+
# Example:
|
|
228
|
+
# Category.each_with_level(Category.root.self_and_descendants, :sort_by_this_column) do |o, level|
|
|
229
|
+
def sorted_each_with_level(objects, order)
|
|
230
|
+
path = [nil]
|
|
231
|
+
children = []
|
|
232
|
+
objects.each do |o|
|
|
233
|
+
children << o if o.leaf?
|
|
234
|
+
if o.parent_id != path.last
|
|
235
|
+
if !children.empty? && !o.leaf?
|
|
236
|
+
children.sort_by! &order
|
|
237
|
+
children.each { |c| yield(c, path.length-1) }
|
|
238
|
+
children = []
|
|
239
|
+
end
|
|
240
|
+
# we are on a new level, did we decent or ascent?
|
|
241
|
+
if path.include?(o.parent_id)
|
|
242
|
+
# remove wrong wrong tailing paths elements
|
|
243
|
+
path.pop while path.last != o.parent_id
|
|
244
|
+
else
|
|
245
|
+
path << o.parent_id
|
|
246
|
+
end
|
|
247
|
+
end
|
|
248
|
+
yield(o,path.length-1) if !o.leaf?
|
|
249
|
+
end
|
|
250
|
+
if !children.empty?
|
|
251
|
+
children.sort_by! &order
|
|
252
|
+
children.each { |c| yield(c, path.length-1) }
|
|
253
|
+
end
|
|
254
|
+
end
|
|
212
255
|
end
|
|
213
|
-
|
|
256
|
+
|
|
214
257
|
# 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.
|
|
215
258
|
#
|
|
216
259
|
# category.self_and_descendants.count
|
|
217
260
|
# category.ancestors.find(:all, :conditions => "name like '%foo%'")
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
end
|
|
261
|
+
# Value of the parent column
|
|
262
|
+
def parent_id
|
|
263
|
+
self[parent_column_name]
|
|
264
|
+
end
|
|
223
265
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
266
|
+
# Value of the left column
|
|
267
|
+
def left
|
|
268
|
+
self[left_column_name]
|
|
269
|
+
end
|
|
228
270
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
271
|
+
# Value of the right column
|
|
272
|
+
def right
|
|
273
|
+
self[right_column_name]
|
|
274
|
+
end
|
|
233
275
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
276
|
+
# Returns true if this is a root node.
|
|
277
|
+
def root?
|
|
278
|
+
parent_id.nil?
|
|
279
|
+
end
|
|
238
280
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
281
|
+
# Returns true if this is the end of a branch.
|
|
282
|
+
def leaf?
|
|
283
|
+
persisted? && right.to_i - left.to_i == 1
|
|
284
|
+
end
|
|
242
285
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
286
|
+
# Returns true is this is a child node
|
|
287
|
+
def child?
|
|
288
|
+
!parent_id.nil?
|
|
289
|
+
end
|
|
247
290
|
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
291
|
+
# Returns root
|
|
292
|
+
def root
|
|
293
|
+
self_and_ancestors.where(parent_column_name => nil).first
|
|
294
|
+
end
|
|
252
295
|
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
296
|
+
# Returns the array of all parents and self
|
|
297
|
+
def self_and_ancestors
|
|
298
|
+
nested_set_scope.where([
|
|
299
|
+
"#{self.class.quoted_table_name}.#{quoted_left_column_name} <= ? AND #{self.class.quoted_table_name}.#{quoted_right_column_name} >= ?", left, right
|
|
300
|
+
])
|
|
301
|
+
end
|
|
259
302
|
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
303
|
+
# Returns an array of all parents
|
|
304
|
+
def ancestors
|
|
305
|
+
without_self self_and_ancestors
|
|
306
|
+
end
|
|
264
307
|
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
308
|
+
# Returns the array of all children of the parent, including self
|
|
309
|
+
def self_and_siblings
|
|
310
|
+
nested_set_scope.where(parent_column_name => parent_id)
|
|
311
|
+
end
|
|
269
312
|
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
313
|
+
# Returns the array of all children of the parent, except self
|
|
314
|
+
def siblings
|
|
315
|
+
without_self self_and_siblings
|
|
316
|
+
end
|
|
274
317
|
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
318
|
+
# Returns a set of all of its nested children which do not have children
|
|
319
|
+
def leaves
|
|
320
|
+
descendants.where("#{self.class.quoted_table_name}.#{quoted_right_column_name} - #{self.class.quoted_table_name}.#{quoted_left_column_name} = 1")
|
|
321
|
+
end
|
|
279
322
|
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
323
|
+
# Returns the level of this object in the tree
|
|
324
|
+
# root level is 0
|
|
325
|
+
def level
|
|
326
|
+
parent_id.nil? ? 0 : ancestors.count
|
|
327
|
+
end
|
|
285
328
|
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
329
|
+
# Returns a set of itself and all of its nested children
|
|
330
|
+
def self_and_descendants
|
|
331
|
+
nested_set_scope.where([
|
|
332
|
+
"#{self.class.quoted_table_name}.#{quoted_left_column_name} >= ? AND #{self.class.quoted_table_name}.#{quoted_right_column_name} <= ?", left, right
|
|
333
|
+
])
|
|
334
|
+
end
|
|
292
335
|
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
336
|
+
# Returns a set of all of its children and nested children
|
|
337
|
+
def descendants
|
|
338
|
+
without_self self_and_descendants
|
|
339
|
+
end
|
|
297
340
|
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
341
|
+
def is_descendant_of?(other)
|
|
342
|
+
other.left < self.left && self.left < other.right && same_scope?(other)
|
|
343
|
+
end
|
|
301
344
|
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
345
|
+
def is_or_is_descendant_of?(other)
|
|
346
|
+
other.left <= self.left && self.left < other.right && same_scope?(other)
|
|
347
|
+
end
|
|
305
348
|
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
349
|
+
def is_ancestor_of?(other)
|
|
350
|
+
self.left < other.left && other.left < self.right && same_scope?(other)
|
|
351
|
+
end
|
|
309
352
|
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
353
|
+
def is_or_is_ancestor_of?(other)
|
|
354
|
+
self.left <= other.left && other.left < self.right && same_scope?(other)
|
|
355
|
+
end
|
|
313
356
|
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
end
|
|
357
|
+
# Check if other model is in the same scope
|
|
358
|
+
def same_scope?(other)
|
|
359
|
+
Array(acts_as_nested_set_options[:scope]).all? do |attr|
|
|
360
|
+
self.send(attr) == other.send(attr)
|
|
319
361
|
end
|
|
362
|
+
end
|
|
320
363
|
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
364
|
+
# Find the first sibling to the left
|
|
365
|
+
def left_sibling
|
|
366
|
+
siblings.where(["#{self.class.quoted_table_name}.#{quoted_left_column_name} < ?", left]).
|
|
367
|
+
order("#{self.class.quoted_table_name}.#{quoted_left_column_name} DESC").last
|
|
368
|
+
end
|
|
326
369
|
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
370
|
+
# Find the first sibling to the right
|
|
371
|
+
def right_sibling
|
|
372
|
+
siblings.where(["#{self.class.quoted_table_name}.#{quoted_left_column_name} > ?", left]).first
|
|
373
|
+
end
|
|
331
374
|
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
375
|
+
# Shorthand method for finding the left sibling and moving to the left of it.
|
|
376
|
+
def move_left
|
|
377
|
+
move_to_left_of left_sibling
|
|
378
|
+
end
|
|
336
379
|
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
380
|
+
# Shorthand method for finding the right sibling and moving to the right of it.
|
|
381
|
+
def move_right
|
|
382
|
+
move_to_right_of right_sibling
|
|
383
|
+
end
|
|
341
384
|
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
385
|
+
# Move the node to the left of another node (you can pass id only)
|
|
386
|
+
def move_to_left_of(node)
|
|
387
|
+
move_to node, :left
|
|
388
|
+
end
|
|
346
389
|
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
390
|
+
# Move the node to the left of another node (you can pass id only)
|
|
391
|
+
def move_to_right_of(node)
|
|
392
|
+
move_to node, :right
|
|
393
|
+
end
|
|
351
394
|
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
395
|
+
# Move the node to the child of another node (you can pass id only)
|
|
396
|
+
def move_to_child_of(node)
|
|
397
|
+
move_to node, :child
|
|
398
|
+
end
|
|
356
399
|
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
400
|
+
# Move the node to root nodes
|
|
401
|
+
def move_to_root
|
|
402
|
+
move_to nil, :root
|
|
403
|
+
end
|
|
361
404
|
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
405
|
+
def move_possible?(target)
|
|
406
|
+
self != target && # Can't target self
|
|
407
|
+
same_scope?(target) && # can't be in different scopes
|
|
408
|
+
# !(left..right).include?(target.left..target.right) # this needs tested more
|
|
409
|
+
# detect impossible move
|
|
410
|
+
!((left <= target.left && right >= target.left) or (left <= target.right && right >= target.right))
|
|
411
|
+
end
|
|
369
412
|
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
413
|
+
def to_text
|
|
414
|
+
self_and_descendants.map do |node|
|
|
415
|
+
"#{'*'*(node.level+1)} #{node.id} #{node.to_s} (#{node.parent_id}, #{node.left}, #{node.right})"
|
|
416
|
+
end.join("\n")
|
|
417
|
+
end
|
|
375
418
|
|
|
376
|
-
|
|
419
|
+
protected
|
|
377
420
|
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
421
|
+
def without_self(scope)
|
|
422
|
+
scope.where(["#{self.class.quoted_table_name}.#{self.class.primary_key} != ?", self])
|
|
423
|
+
end
|
|
381
424
|
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
425
|
+
# All nested set queries should use this nested_set_scope, which performs finds on
|
|
426
|
+
# the base ActiveRecord class, using the :scope declared in the acts_as_nested_set
|
|
427
|
+
# declaration.
|
|
428
|
+
def nested_set_scope(options = {})
|
|
429
|
+
options = {:order => quoted_left_column_name}.merge(options)
|
|
430
|
+
scopes = Array(acts_as_nested_set_options[:scope])
|
|
431
|
+
options[:conditions] = scopes.inject({}) do |conditions,attr|
|
|
432
|
+
conditions.merge attr => self[attr]
|
|
433
|
+
end unless scopes.empty?
|
|
434
|
+
self.class.base_class.unscoped.scoped options
|
|
435
|
+
end
|
|
436
|
+
|
|
437
|
+
def store_new_parent
|
|
438
|
+
@move_to_new_parent_id = send("#{parent_column_name}_changed?") ? parent_id : false
|
|
439
|
+
true # force callback to return true
|
|
440
|
+
end
|
|
393
441
|
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
442
|
+
def move_to_new_parent
|
|
443
|
+
if @move_to_new_parent_id.nil?
|
|
444
|
+
move_to_root
|
|
445
|
+
elsif @move_to_new_parent_id
|
|
446
|
+
move_to_child_of(@move_to_new_parent_id)
|
|
397
447
|
end
|
|
448
|
+
end
|
|
398
449
|
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
450
|
+
def set_depth!
|
|
451
|
+
if nested_set_scope.column_names.map(&:to_s).include?(depth_column_name.to_s)
|
|
452
|
+
in_tenacious_transaction do
|
|
453
|
+
reload
|
|
454
|
+
|
|
455
|
+
nested_set_scope.where(:id => id).update_all(["#{quoted_depth_column_name} = ?", level])
|
|
404
456
|
end
|
|
457
|
+
self[:depth] = self.level
|
|
405
458
|
end
|
|
459
|
+
end
|
|
406
460
|
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
461
|
+
# on creation, set automatically lft and rgt to the end of the tree
|
|
462
|
+
def set_default_left_and_right
|
|
463
|
+
highest_right_row = nested_set_scope(:order => "#{quoted_right_column_name} desc").find(:first, :limit => 1,:lock => true )
|
|
464
|
+
maxright = highest_right_row ? (highest_right_row[right_column_name] || 0) : 0
|
|
465
|
+
# adds the new node to the right of all existing nodes
|
|
466
|
+
self[left_column_name] = maxright + 1
|
|
467
|
+
self[right_column_name] = maxright + 2
|
|
468
|
+
end
|
|
414
469
|
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
470
|
+
def in_tenacious_transaction(&block)
|
|
471
|
+
retry_count = 0
|
|
472
|
+
begin
|
|
473
|
+
transaction(&block)
|
|
474
|
+
rescue ActiveRecord::StatementInvalid => error
|
|
475
|
+
raise unless connection.open_transactions.zero?
|
|
476
|
+
raise unless error.message =~ /Deadlock found when trying to get lock|Lock wait timeout exceeded/
|
|
477
|
+
raise unless retry_count < 10
|
|
478
|
+
retry_count += 1
|
|
479
|
+
logger.info "Deadlock detected on retry #{retry_count}, restarting transaction"
|
|
480
|
+
sleep(rand(retry_count)*0.1) # Aloha protocol
|
|
481
|
+
retry
|
|
482
|
+
end
|
|
483
|
+
end
|
|
419
484
|
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
485
|
+
# Prunes a branch off of the tree, shifting all of the elements on the right
|
|
486
|
+
# back to the left so the counts still work.
|
|
487
|
+
def destroy_descendants
|
|
488
|
+
return if right.nil? || left.nil? || skip_before_destroy
|
|
489
|
+
|
|
490
|
+
in_tenacious_transaction do
|
|
491
|
+
reload_nested_set
|
|
492
|
+
# select the rows in the model that extend past the deletion point and apply a lock
|
|
493
|
+
self.class.base_class.find(:all,
|
|
494
|
+
:select => "id",
|
|
495
|
+
:conditions => ["#{quoted_left_column_name} >= ?", left],
|
|
496
|
+
:lock => true
|
|
497
|
+
)
|
|
498
|
+
|
|
499
|
+
if acts_as_nested_set_options[:dependent] == :destroy
|
|
500
|
+
descendants.each do |model|
|
|
501
|
+
model.skip_before_destroy = true
|
|
502
|
+
model.destroy
|
|
431
503
|
end
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
["#{quoted_left_column_name} = (#{quoted_left_column_name} - ?)", diff],
|
|
437
|
-
["#{quoted_left_column_name} > ?", right]
|
|
438
|
-
)
|
|
439
|
-
nested_set_scope.update_all(
|
|
440
|
-
["#{quoted_right_column_name} = (#{quoted_right_column_name} - ?)", diff],
|
|
441
|
-
["#{quoted_right_column_name} > ?", right]
|
|
504
|
+
else
|
|
505
|
+
nested_set_scope.delete_all(
|
|
506
|
+
["#{quoted_left_column_name} > ? AND #{quoted_right_column_name} < ?",
|
|
507
|
+
left, right]
|
|
442
508
|
)
|
|
443
|
-
|
|
444
|
-
# Don't allow multiple calls to destroy to corrupt the set
|
|
445
|
-
self.skip_before_destroy = true
|
|
446
509
|
end
|
|
447
|
-
end
|
|
448
510
|
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
"#{
|
|
511
|
+
# update lefts and rights for remaining nodes
|
|
512
|
+
diff = right - left + 1
|
|
513
|
+
nested_set_scope.update_all(
|
|
514
|
+
["#{quoted_left_column_name} = (#{quoted_left_column_name} - ?)", diff],
|
|
515
|
+
["#{quoted_left_column_name} > ?", right]
|
|
516
|
+
)
|
|
517
|
+
nested_set_scope.update_all(
|
|
518
|
+
["#{quoted_right_column_name} = (#{quoted_right_column_name} - ?)", diff],
|
|
519
|
+
["#{quoted_right_column_name} > ?", right]
|
|
520
|
+
)
|
|
521
|
+
|
|
522
|
+
# Don't allow multiple calls to destroy to corrupt the set
|
|
523
|
+
self.skip_before_destroy = true
|
|
453
524
|
end
|
|
525
|
+
end
|
|
454
526
|
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
# load object if node is not an object
|
|
463
|
-
target = nested_set_scope.find(target)
|
|
464
|
-
end
|
|
465
|
-
self.reload_nested_set
|
|
527
|
+
# reload left, right, and parent
|
|
528
|
+
def reload_nested_set
|
|
529
|
+
reload(
|
|
530
|
+
:select => "#{quoted_left_column_name}, #{quoted_right_column_name}, #{quoted_parent_column_name}",
|
|
531
|
+
:lock => true
|
|
532
|
+
)
|
|
533
|
+
end
|
|
466
534
|
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
535
|
+
def move_to(target, position)
|
|
536
|
+
raise ActiveRecord::ActiveRecordError, "You cannot move a new node" if self.new_record?
|
|
537
|
+
run_callbacks :move do
|
|
538
|
+
in_tenacious_transaction do
|
|
539
|
+
if target.is_a? self.class.base_class
|
|
540
|
+
target.reload_nested_set
|
|
541
|
+
elsif position != :root
|
|
542
|
+
# load object if node is not an object
|
|
543
|
+
target = nested_set_scope.find(target)
|
|
544
|
+
end
|
|
545
|
+
self.reload_nested_set
|
|
470
546
|
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
when :right; target[right_column_name] + 1
|
|
475
|
-
when :root; 1
|
|
476
|
-
else raise ActiveRecord::ActiveRecordError, "Position should be :child, :left, :right or :root ('#{position}' received)."
|
|
477
|
-
end
|
|
547
|
+
unless position == :root || move_possible?(target)
|
|
548
|
+
raise ActiveRecord::ActiveRecordError, "Impossible move, target node cannot be inside moved tree."
|
|
549
|
+
end
|
|
478
550
|
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
551
|
+
bound = case position
|
|
552
|
+
when :child; target[right_column_name]
|
|
553
|
+
when :left; target[left_column_name]
|
|
554
|
+
when :right; target[right_column_name] + 1
|
|
555
|
+
when :root; 1
|
|
556
|
+
else raise ActiveRecord::ActiveRecordError, "Position should be :child, :left, :right or :root ('#{position}' received)."
|
|
557
|
+
end
|
|
558
|
+
|
|
559
|
+
if bound > self[right_column_name]
|
|
560
|
+
bound = bound - 1
|
|
561
|
+
other_bound = self[right_column_name] + 1
|
|
562
|
+
else
|
|
563
|
+
other_bound = self[left_column_name] - 1
|
|
564
|
+
end
|
|
485
565
|
|
|
486
|
-
|
|
487
|
-
|
|
566
|
+
# there would be no change
|
|
567
|
+
return if bound == self[right_column_name] || bound == self[left_column_name]
|
|
488
568
|
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
569
|
+
# we have defined the boundaries of two non-overlapping intervals,
|
|
570
|
+
# so sorting puts both the intervals and their boundaries in order
|
|
571
|
+
a, b, c, d = [self[left_column_name], self[right_column_name], bound, other_bound].sort
|
|
492
572
|
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
end
|
|
573
|
+
# select the rows in the model between a and d, and apply a lock
|
|
574
|
+
self.class.base_class.select('id').lock(true).where(
|
|
575
|
+
["#{quoted_left_column_name} >= :a and #{quoted_right_column_name} <= :d", {:a => a, :d => d}]
|
|
576
|
+
)
|
|
498
577
|
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
"WHEN #{quoted_left_column_name} BETWEEN :c AND :d " +
|
|
504
|
-
"THEN #{quoted_left_column_name} + :a - :c " +
|
|
505
|
-
"ELSE #{quoted_left_column_name} END, " +
|
|
506
|
-
"#{quoted_right_column_name} = CASE " +
|
|
507
|
-
"WHEN #{quoted_right_column_name} BETWEEN :a AND :b " +
|
|
508
|
-
"THEN #{quoted_right_column_name} + :d - :b " +
|
|
509
|
-
"WHEN #{quoted_right_column_name} BETWEEN :c AND :d " +
|
|
510
|
-
"THEN #{quoted_right_column_name} + :a - :c " +
|
|
511
|
-
"ELSE #{quoted_right_column_name} END, " +
|
|
512
|
-
"#{quoted_parent_column_name} = CASE " +
|
|
513
|
-
"WHEN #{self.class.base_class.primary_key} = :id THEN :new_parent " +
|
|
514
|
-
"ELSE #{quoted_parent_column_name} END",
|
|
515
|
-
{:a => a, :b => b, :c => c, :d => d, :id => self.id, :new_parent => new_parent}
|
|
516
|
-
])
|
|
578
|
+
new_parent = case position
|
|
579
|
+
when :child; target.id
|
|
580
|
+
when :root; nil
|
|
581
|
+
else target[parent_column_name]
|
|
517
582
|
end
|
|
518
|
-
|
|
519
|
-
self.
|
|
583
|
+
|
|
584
|
+
self.nested_set_scope.update_all([
|
|
585
|
+
"#{quoted_left_column_name} = CASE " +
|
|
586
|
+
"WHEN #{quoted_left_column_name} BETWEEN :a AND :b " +
|
|
587
|
+
"THEN #{quoted_left_column_name} + :d - :b " +
|
|
588
|
+
"WHEN #{quoted_left_column_name} BETWEEN :c AND :d " +
|
|
589
|
+
"THEN #{quoted_left_column_name} + :a - :c " +
|
|
590
|
+
"ELSE #{quoted_left_column_name} END, " +
|
|
591
|
+
"#{quoted_right_column_name} = CASE " +
|
|
592
|
+
"WHEN #{quoted_right_column_name} BETWEEN :a AND :b " +
|
|
593
|
+
"THEN #{quoted_right_column_name} + :d - :b " +
|
|
594
|
+
"WHEN #{quoted_right_column_name} BETWEEN :c AND :d " +
|
|
595
|
+
"THEN #{quoted_right_column_name} + :a - :c " +
|
|
596
|
+
"ELSE #{quoted_right_column_name} END, " +
|
|
597
|
+
"#{quoted_parent_column_name} = CASE " +
|
|
598
|
+
"WHEN #{self.class.base_class.primary_key} = :id THEN :new_parent " +
|
|
599
|
+
"ELSE #{quoted_parent_column_name} END",
|
|
600
|
+
{:a => a, :b => b, :c => c, :d => d, :id => self.id, :new_parent => new_parent}
|
|
601
|
+
])
|
|
520
602
|
end
|
|
603
|
+
target.reload_nested_set if target
|
|
604
|
+
self.set_depth!
|
|
605
|
+
self.reload_nested_set
|
|
521
606
|
end
|
|
522
|
-
|
|
523
607
|
end
|
|
524
608
|
|
|
525
609
|
end
|
|
@@ -534,6 +618,10 @@ module CollectiveIdea #:nodoc:
|
|
|
534
618
|
acts_as_nested_set_options[:right_column]
|
|
535
619
|
end
|
|
536
620
|
|
|
621
|
+
def depth_column_name
|
|
622
|
+
acts_as_nested_set_options[:depth_column]
|
|
623
|
+
end
|
|
624
|
+
|
|
537
625
|
def parent_column_name
|
|
538
626
|
acts_as_nested_set_options[:parent_column]
|
|
539
627
|
end
|
|
@@ -550,6 +638,10 @@ module CollectiveIdea #:nodoc:
|
|
|
550
638
|
connection.quote_column_name(right_column_name)
|
|
551
639
|
end
|
|
552
640
|
|
|
641
|
+
def quoted_depth_column_name
|
|
642
|
+
connection.quote_column_name(depth_column_name)
|
|
643
|
+
end
|
|
644
|
+
|
|
553
645
|
def quoted_parent_column_name
|
|
554
646
|
connection.quote_column_name(parent_column_name)
|
|
555
647
|
end
|
|
@@ -561,4 +653,5 @@ module CollectiveIdea #:nodoc:
|
|
|
561
653
|
|
|
562
654
|
end
|
|
563
655
|
end
|
|
564
|
-
end
|
|
656
|
+
end
|
|
657
|
+
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
1
2
|
module CollectiveIdea #:nodoc:
|
|
2
3
|
module Acts #:nodoc:
|
|
3
4
|
module NestedSet #:nodoc:
|
|
@@ -21,8 +22,12 @@ module CollectiveIdea #:nodoc:
|
|
|
21
22
|
# }) %>
|
|
22
23
|
#
|
|
23
24
|
def nested_set_options(class_or_item, mover = nil)
|
|
24
|
-
|
|
25
|
-
|
|
25
|
+
if class_or_item.is_a? Array
|
|
26
|
+
items = class_or_item.reject { |e| !e.root? }
|
|
27
|
+
else
|
|
28
|
+
class_or_item = class_or_item.roots if class_or_item.is_a?(Class)
|
|
29
|
+
items = Array(class_or_item)
|
|
30
|
+
end
|
|
26
31
|
result = []
|
|
27
32
|
items.each do |root|
|
|
28
33
|
result += root.self_and_descendants.map do |i|
|
|
@@ -33,8 +38,52 @@ module CollectiveIdea #:nodoc:
|
|
|
33
38
|
end
|
|
34
39
|
result
|
|
35
40
|
end
|
|
36
|
-
|
|
41
|
+
|
|
42
|
+
# Returns options for select as nested_set_options, sorted by an specific column
|
|
43
|
+
# It requires passing a string with the name of the column to sort the set with
|
|
44
|
+
# You can exclude some items from the tree.
|
|
45
|
+
# You can pass a block receiving an item and returning the string displayed in the select.
|
|
46
|
+
#
|
|
47
|
+
# == Params
|
|
48
|
+
# * +class_or_item+ - Class name or top level times
|
|
49
|
+
# * +:column+ - Column to sort the set (this will sort each children for all root elements)
|
|
50
|
+
# * +mover+ - The item that is being move, used to exlude impossible moves
|
|
51
|
+
# * +&block+ - a block that will be used to display: { |item| ... item.name }
|
|
52
|
+
#
|
|
53
|
+
# == Usage
|
|
54
|
+
#
|
|
55
|
+
# <%= f.select :parent_id, nested_set_options(Category, :sort_by_this_column, @category) {|i|
|
|
56
|
+
# "#{'–' * i.level} #{i.name}"
|
|
57
|
+
# }) %>
|
|
58
|
+
#
|
|
59
|
+
def sorted_nested_set_options(class_or_item, order, mover = nil)
|
|
60
|
+
if class_or_item.is_a? Array
|
|
61
|
+
items = class_or_item.reject { |e| !e.root? }
|
|
62
|
+
else
|
|
63
|
+
class_or_item = class_or_item.roots if class_or_item.is_a?(Class)
|
|
64
|
+
items = Array(class_or_item)
|
|
65
|
+
end
|
|
66
|
+
result = []
|
|
67
|
+
children = []
|
|
68
|
+
items.each do |root|
|
|
69
|
+
root.self_and_descendants.map do |i|
|
|
70
|
+
if mover.nil? || mover.new_record? || mover.move_possible?(i)
|
|
71
|
+
if !i.leaf?
|
|
72
|
+
children.sort_by! &order
|
|
73
|
+
children.each { |c| result << [yield(c), c.id] }
|
|
74
|
+
children = []
|
|
75
|
+
result << [yield(i), i.id]
|
|
76
|
+
else
|
|
77
|
+
children << i
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end.compact
|
|
81
|
+
end
|
|
82
|
+
children.sort_by! &order
|
|
83
|
+
children.each { |c| result << [yield(c), c.id] }
|
|
84
|
+
result
|
|
85
|
+
end
|
|
37
86
|
end
|
|
38
87
|
end
|
|
39
88
|
end
|
|
40
|
-
end
|
|
89
|
+
end
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
module AwesomeNestedSet
|
|
2
|
-
VERSION = '2.
|
|
3
|
-
end
|
|
2
|
+
VERSION = '2.1.1' unless defined?(::AwesomeNestedSet::VERSION)
|
|
3
|
+
end
|
metadata
CHANGED
|
@@ -1,18 +1,23 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: awesome_nested_set
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
+
hash: 9
|
|
4
5
|
prerelease:
|
|
5
|
-
|
|
6
|
+
segments:
|
|
7
|
+
- 2
|
|
8
|
+
- 1
|
|
9
|
+
- 1
|
|
10
|
+
version: 2.1.1
|
|
6
11
|
platform: ruby
|
|
7
12
|
authors:
|
|
8
13
|
- Brandon Keepers
|
|
9
14
|
- Daniel Morrison
|
|
15
|
+
- Philip Arndt
|
|
10
16
|
autorequire:
|
|
11
17
|
bindir: bin
|
|
12
18
|
cert_chain: []
|
|
13
19
|
|
|
14
|
-
date:
|
|
15
|
-
default_executable:
|
|
20
|
+
date: 2012-01-25 00:00:00 Z
|
|
16
21
|
dependencies:
|
|
17
22
|
- !ruby/object:Gem::Dependency
|
|
18
23
|
name: activerecord
|
|
@@ -22,6 +27,11 @@ dependencies:
|
|
|
22
27
|
requirements:
|
|
23
28
|
- - ">="
|
|
24
29
|
- !ruby/object:Gem::Version
|
|
30
|
+
hash: 7
|
|
31
|
+
segments:
|
|
32
|
+
- 3
|
|
33
|
+
- 0
|
|
34
|
+
- 0
|
|
25
35
|
version: 3.0.0
|
|
26
36
|
type: :runtime
|
|
27
37
|
version_requirements: *id001
|
|
@@ -41,7 +51,6 @@ files:
|
|
|
41
51
|
- MIT-LICENSE
|
|
42
52
|
- README.rdoc
|
|
43
53
|
- CHANGELOG
|
|
44
|
-
has_rdoc: true
|
|
45
54
|
homepage: http://github.com/collectiveidea/awesome_nested_set
|
|
46
55
|
licenses: []
|
|
47
56
|
|
|
@@ -58,17 +67,23 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
58
67
|
requirements:
|
|
59
68
|
- - ">="
|
|
60
69
|
- !ruby/object:Gem::Version
|
|
70
|
+
hash: 3
|
|
71
|
+
segments:
|
|
72
|
+
- 0
|
|
61
73
|
version: "0"
|
|
62
74
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
63
75
|
none: false
|
|
64
76
|
requirements:
|
|
65
77
|
- - ">="
|
|
66
78
|
- !ruby/object:Gem::Version
|
|
79
|
+
hash: 3
|
|
80
|
+
segments:
|
|
81
|
+
- 0
|
|
67
82
|
version: "0"
|
|
68
83
|
requirements: []
|
|
69
84
|
|
|
70
85
|
rubyforge_project:
|
|
71
|
-
rubygems_version: 1.
|
|
86
|
+
rubygems_version: 1.8.10
|
|
72
87
|
signing_key:
|
|
73
88
|
specification_version: 3
|
|
74
89
|
summary: An awesome nested set implementation for Active Record
|