jnicklas-eb_nested_set 0.3.2 → 0.3.5
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +33 -1
- data/Rakefile +6 -1
- data/lib/eb_nested_set.rb +193 -72
- metadata +2 -2
data/README.md
CHANGED
@@ -2,6 +2,30 @@
|
|
2
2
|
|
3
3
|
A nested set is a datastruture in a database, sort of like a tree, but unlike a tree it allows you to find all descendants of a node with a single query. Loading a deeply nested structure with nested sets is therefore a lot more efficient than using a tree. So what's the disadvantage? Nested sets are a lot harder to maintain, since inserting and moving records requires management and it is easy to corrupt the dataset. Enter: EvenBetterNestedSet. Amount of micromanaging you need to do: 0. EvenBetterNestedSet does it all for you.
|
4
4
|
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
Stable:
|
8
|
+
|
9
|
+
[sudo] gem install eb_nested_set
|
10
|
+
|
11
|
+
Edge:
|
12
|
+
|
13
|
+
[sudo] gem install jnicklas-eb_nested_set --source http://gems.github.com
|
14
|
+
|
15
|
+
From source:
|
16
|
+
|
17
|
+
git clone git://github.com/jnicklas/even_better_nested_set.git
|
18
|
+
cd even_better_nested_set
|
19
|
+
rake install
|
20
|
+
|
21
|
+
If you're running Rails, just add it to your environment.rb file
|
22
|
+
|
23
|
+
config.gem 'eb_nested_set'
|
24
|
+
|
25
|
+
You can also install it as a Rails plugin.
|
26
|
+
|
27
|
+
script/plugin install git://github.com/jnicklas/even_better_nested_set.git
|
28
|
+
|
5
29
|
## Declaring nested sets
|
6
30
|
|
7
31
|
This is how you declare a nested set:
|
@@ -38,4 +62,12 @@ EvenBetterNestedSet will not automatically cache children for you, because it as
|
|
38
62
|
|
39
63
|
or more conveniently:
|
40
64
|
|
41
|
-
d = Directory.find_with_nested_set(42)
|
65
|
+
d = Directory.find_with_nested_set(42)
|
66
|
+
|
67
|
+
## I18n
|
68
|
+
|
69
|
+
Add these keys to your translation file:
|
70
|
+
|
71
|
+
even_better_nested_set:
|
72
|
+
parent_not_in_scope: "nay, thy parent not be in scope {{scope_name}}"
|
73
|
+
illegal_nesting: "arr tis be illegal nesting"
|
data/Rakefile
CHANGED
@@ -3,9 +3,10 @@ require 'rake/gempackagetask'
|
|
3
3
|
require 'rubygems/specification'
|
4
4
|
require 'date'
|
5
5
|
require 'spec/rake/spectask'
|
6
|
+
require 'yard'
|
6
7
|
|
7
8
|
GEM = "eb_nested_set"
|
8
|
-
GEM_VERSION = "0.3.
|
9
|
+
GEM_VERSION = "0.3.5"
|
9
10
|
AUTHOR = "Jonas Nicklas"
|
10
11
|
EMAIL = "jonas.nicklas@gmail.com"
|
11
12
|
HOMEPAGE = "http://github.com/jnicklas/even_better_nested_set/tree/master"
|
@@ -27,6 +28,10 @@ spec = Gem::Specification.new do |s|
|
|
27
28
|
s.files = %w(LICENSE README.md Rakefile init.rb) + Dir.glob("{lib,spec}/**/*")
|
28
29
|
end
|
29
30
|
|
31
|
+
YARD::Rake::YardocTask.new do |t|
|
32
|
+
t.files = ["README.md", "LICENSE", "TODO", 'lib/**/*.rb']
|
33
|
+
end
|
34
|
+
|
30
35
|
Rake::GemPackageTask.new(spec) do |pkg|
|
31
36
|
pkg.gem_spec = spec
|
32
37
|
end
|
data/lib/eb_nested_set.rb
CHANGED
@@ -1,13 +1,57 @@
|
|
1
1
|
module EvenBetterNestedSet
|
2
2
|
|
3
|
-
def self.included(base)
|
4
|
-
super
|
5
|
-
base.extend ClassMethods
|
6
|
-
end
|
7
|
-
|
8
3
|
class NestedSetError < StandardError; end
|
9
4
|
class IllegalAssignmentError < NestedSetError; end
|
10
|
-
|
5
|
+
|
6
|
+
##
|
7
|
+
# Declare this model as a nested set. Automatically adds all methods in +NestedSet+ to the model, as
|
8
|
+
# well as parent and children associations.
|
9
|
+
#
|
10
|
+
# == Options
|
11
|
+
# left [Symbol]:: the name of the column that contains the left boundary [Defaults to +left+]
|
12
|
+
# right [Symbol]:: the name of the column that contains the right boundary [Defaults to +right+]
|
13
|
+
# scope [Symbol]:: the name of an association to scope this nested set to
|
14
|
+
#
|
15
|
+
# @param [Hash] options a set of options
|
16
|
+
#
|
17
|
+
def acts_as_nested_set(options={})
|
18
|
+
options = { :left => :left, :right => :right }.merge!(options)
|
19
|
+
options[:scope] = "#{options[:scope]}_id" if options[:scope]
|
20
|
+
|
21
|
+
include NestedSet
|
22
|
+
|
23
|
+
self.nested_set_options = options
|
24
|
+
|
25
|
+
class_eval <<-RUBY, __FILE__, __LINE__+1
|
26
|
+
def #{options[:left]}=(left)
|
27
|
+
raise EvenBetterNestedSet::IllegalAssignmentError, "#{options[:left]} is an internal attribute used by EvenBetterNestedSet, do not assign it directly as is may corrupt the data in your database"
|
28
|
+
end
|
29
|
+
|
30
|
+
def #{options[:right]}=(right)
|
31
|
+
raise EvenBetterNestedSet::IllegalAssignmentError, "#{options[:right]} is an internal attribute used by EvenBetterNestedSet, do not assign it directly as is may corrupt the data in your database"
|
32
|
+
end
|
33
|
+
RUBY
|
34
|
+
|
35
|
+
named_scope :roots, :conditions => { :parent_id => nil }, :order => "#{nested_set_column(:left)} asc"
|
36
|
+
has_many :children, :class_name => self.name, :foreign_key => :parent_id, :order => "#{nested_set_column(:left)} asc"
|
37
|
+
belongs_to :parent, :class_name => self.name, :foreign_key => :parent_id
|
38
|
+
|
39
|
+
named_scope :descendants, lambda { |node|
|
40
|
+
left, right = find_boundaries(node.id)
|
41
|
+
{ :conditions => ["#{nested_set_column(:left)} > ? and #{nested_set_column(:right)} < ?", left, right],
|
42
|
+
:order => "#{nested_set_column(:left)} asc" }
|
43
|
+
}
|
44
|
+
|
45
|
+
before_create :append_node
|
46
|
+
before_update :move_node
|
47
|
+
before_destroy :reload
|
48
|
+
after_destroy :remove_node
|
49
|
+
validate_on_update :illegal_nesting
|
50
|
+
validate :validate_parent_is_within_scope
|
51
|
+
|
52
|
+
delegate :nested_set_column, :to => "self.class"
|
53
|
+
end
|
54
|
+
|
11
55
|
module NestedSet
|
12
56
|
|
13
57
|
def self.included(base)
|
@@ -19,10 +63,20 @@ module EvenBetterNestedSet
|
|
19
63
|
|
20
64
|
attr_accessor :nested_set_options
|
21
65
|
|
66
|
+
##
|
67
|
+
# Finds the last root, used internally to find the point to insert new roots
|
68
|
+
#
|
69
|
+
# @return [ActiveRecord::Base] the last root node
|
70
|
+
#
|
22
71
|
def find_last_root
|
23
72
|
find(:first, :order => "#{nested_set_column(:right)} DESC", :conditions => { :parent_id => nil })
|
24
73
|
end
|
25
74
|
|
75
|
+
##
|
76
|
+
# Finds the left and right boundaries of a node given an id.
|
77
|
+
#
|
78
|
+
# @return [Array[Integer]] left and right boundaries
|
79
|
+
#
|
26
80
|
def find_boundaries(id)
|
27
81
|
query = "SELECT #{nested_set_column(:left)}, #{nested_set_column(:right)}" +
|
28
82
|
"FROM #{quote_db_property(table_name)}" +
|
@@ -30,10 +84,21 @@ module EvenBetterNestedSet
|
|
30
84
|
connection.select_rows(query).first
|
31
85
|
end
|
32
86
|
|
87
|
+
##
|
88
|
+
# Returns all nodes with children cached to a nested set
|
89
|
+
#
|
90
|
+
# @return [Array[ActiveRecord::Base]] an array of root nodes with cached children
|
91
|
+
#
|
33
92
|
def nested_set
|
34
93
|
sort_nodes_to_nested_set(find(:all, :order => "#{nested_set_column(:left)} ASC"))
|
35
94
|
end
|
36
95
|
|
96
|
+
##
|
97
|
+
# Finds all nodes matching the criteria provided, and caches their descendants
|
98
|
+
#
|
99
|
+
# @param [Object] *args same parameters as ordinary find calls
|
100
|
+
# @return [Array[ActiveRecord::Base], ActiveRecord::Base] the found nodes
|
101
|
+
#
|
37
102
|
def find_with_nested_set(*args)
|
38
103
|
result = find(*args)
|
39
104
|
if result.respond_to?(:cache_nested_set)
|
@@ -46,6 +111,12 @@ module EvenBetterNestedSet
|
|
46
111
|
result
|
47
112
|
end
|
48
113
|
|
114
|
+
##
|
115
|
+
# Given a flat list of nodes, sorts them to a tree, caching descendants in the process
|
116
|
+
#
|
117
|
+
# @param [Array[ActiveRecord::Base]] nodes an array of nodes
|
118
|
+
# @return [Array[ActiveRecord::Base]] an array of nodes with children cached
|
119
|
+
#
|
49
120
|
def sort_nodes_to_nested_set(nodes)
|
50
121
|
roots = []
|
51
122
|
hashmap = {}
|
@@ -69,11 +140,18 @@ module EvenBetterNestedSet
|
|
69
140
|
return roots
|
70
141
|
end
|
71
142
|
|
143
|
+
##
|
144
|
+
# Returns the properly quoted column name given the generic term
|
145
|
+
#
|
146
|
+
# @param [Symbol] name the name of the column to find
|
147
|
+
# @return [String]
|
72
148
|
def nested_set_column(name)
|
73
149
|
quote_db_property(nested_set_options[name])
|
74
150
|
end
|
75
151
|
|
152
|
+
##
|
76
153
|
# Recalculates the left and right values for the entire tree
|
154
|
+
#
|
77
155
|
def recalculate_nested_set
|
78
156
|
transaction do
|
79
157
|
left = 1
|
@@ -83,29 +161,59 @@ module EvenBetterNestedSet
|
|
83
161
|
end
|
84
162
|
end
|
85
163
|
|
164
|
+
##
|
165
|
+
# Properly quotes a column name
|
166
|
+
#
|
167
|
+
# @param [String] property
|
168
|
+
# @return [String] quoted property
|
169
|
+
#
|
86
170
|
def quote_db_property(property)
|
87
171
|
"`#{property}`".gsub('.','`.`')
|
88
172
|
end
|
89
173
|
|
90
174
|
end
|
91
175
|
|
176
|
+
##
|
177
|
+
# Checks if this root is a root node
|
178
|
+
#
|
179
|
+
# @return [Boolean] whether this node is a root node or not
|
180
|
+
#
|
92
181
|
def root?
|
93
182
|
not parent_id?
|
94
183
|
end
|
95
184
|
|
185
|
+
##
|
186
|
+
# Checks if this node is a descendant of node
|
187
|
+
#
|
188
|
+
# @param [ActiveRecord::Base] node the node to check agains
|
189
|
+
# @return [Boolean] whether this node is a descendant
|
190
|
+
#
|
96
191
|
def descendant_of?(node)
|
97
192
|
node.left < self.left && self.right < node.right
|
98
193
|
end
|
99
194
|
|
100
|
-
|
101
|
-
|
195
|
+
##
|
196
|
+
# Finds the root node that this node descends from
|
197
|
+
#
|
198
|
+
# @param [Boolean] force_reload forces the root node to be reloaded
|
199
|
+
# @return [ActiveRecord::Base] node the root node this descends from
|
200
|
+
#
|
201
|
+
def root(force_reload=nil)
|
202
|
+
@root = nil if force_reload
|
203
|
+
@root ||= transaction do
|
102
204
|
reload_boundaries
|
103
|
-
|
205
|
+
base_class.roots.find(:first, :conditions => ["#{nested_set_column(:left)} <= ? AND #{nested_set_column(:right)} >= ?", left, right])
|
104
206
|
end
|
105
207
|
end
|
106
208
|
|
107
209
|
alias_method :patriarch, :root
|
108
210
|
|
211
|
+
##
|
212
|
+
# Returns a list of ancestors this node belongs to
|
213
|
+
#
|
214
|
+
# @param [Boolean] force_reload forces the list to be reloaded
|
215
|
+
# @return [Array[ActiveRecord::Base]] a list of nodes that this node descends from
|
216
|
+
#
|
109
217
|
def ancestors(force_reload=false)
|
110
218
|
@ancestors = nil if force_reload
|
111
219
|
@ancestors ||= base_class.find(
|
@@ -114,26 +222,55 @@ module EvenBetterNestedSet
|
|
114
222
|
)
|
115
223
|
end
|
116
224
|
|
225
|
+
##
|
226
|
+
# Returns a list of the node itself and all of its ancestors
|
227
|
+
#
|
228
|
+
# @param [Boolean] force_reload forces the list to be reloaded
|
229
|
+
# @return [Array[ActiveRecord::Base]] a list of nodes that this node descends from
|
230
|
+
#
|
117
231
|
def lineage(force_reload=false)
|
118
232
|
[self, *ancestors(force_reload)]
|
119
233
|
end
|
120
234
|
|
235
|
+
##
|
236
|
+
# Returns all nodes that descend from the same root node as this node
|
237
|
+
#
|
238
|
+
# @return [Array[ActiveRecord::Base]]
|
239
|
+
#
|
121
240
|
def kin
|
122
241
|
patriarch.family
|
123
242
|
end
|
124
243
|
|
244
|
+
##
|
245
|
+
# Returns all nodes that descend from this node
|
246
|
+
#
|
247
|
+
# @return [Array[ActiveRecord::Base]]
|
248
|
+
#
|
125
249
|
def descendants
|
126
250
|
base_class.descendants(self)
|
127
251
|
end
|
128
252
|
|
253
|
+
##
|
254
|
+
# Caches the children of this node
|
255
|
+
#
|
129
256
|
def cache_nested_set
|
130
257
|
@cached_children || base_class.sort_nodes_to_nested_set(family)
|
131
258
|
end
|
132
259
|
|
260
|
+
##
|
261
|
+
# Returns the node and all nodes that descend from it.
|
262
|
+
#
|
263
|
+
# @return [Array[ActiveRecord::Base]]
|
264
|
+
#
|
133
265
|
def family
|
134
266
|
[self, *descendants]
|
135
267
|
end
|
136
268
|
|
269
|
+
##
|
270
|
+
# Returns the ids of the node and all nodes that descend from it.
|
271
|
+
#
|
272
|
+
# @return [Array[Integer]]
|
273
|
+
#
|
137
274
|
def family_ids(force_reload=false)
|
138
275
|
return @family_ids unless @family_ids.nil? or force_reload
|
139
276
|
|
@@ -146,14 +283,29 @@ module EvenBetterNestedSet
|
|
146
283
|
end
|
147
284
|
end
|
148
285
|
|
286
|
+
##
|
287
|
+
# Returns all nodes that share the same parent as this node.
|
288
|
+
#
|
289
|
+
# @return [Array[ActiveRecord::Base]]
|
290
|
+
#
|
149
291
|
def generation
|
150
292
|
root? ? base_class.roots : parent.children
|
151
293
|
end
|
152
294
|
|
295
|
+
##
|
296
|
+
# Returns all nodes that are siblings of this node
|
297
|
+
#
|
298
|
+
# @return [Array[ActiveRecord::Base]]
|
299
|
+
#
|
153
300
|
def siblings
|
154
301
|
generation - [self]
|
155
302
|
end
|
156
303
|
|
304
|
+
##
|
305
|
+
# Returns how deeply this node is nested, that is how many ancestors it has.
|
306
|
+
#
|
307
|
+
# @return [Integer] the number of ancestors of this node.
|
308
|
+
#
|
157
309
|
def level
|
158
310
|
if root?
|
159
311
|
0
|
@@ -164,36 +316,46 @@ module EvenBetterNestedSet
|
|
164
316
|
end
|
165
317
|
end
|
166
318
|
|
319
|
+
##
|
320
|
+
# @return [Range] the left to the right boundary of this node
|
321
|
+
#
|
167
322
|
def bounds
|
168
323
|
left..right
|
169
324
|
end
|
170
325
|
|
171
|
-
|
172
|
-
|
326
|
+
##
|
327
|
+
# @return [Integer] the left boundary of this node
|
328
|
+
#
|
329
|
+
def left
|
330
|
+
read_attribute(self.class.nested_set_options[:left])
|
173
331
|
end
|
174
332
|
|
175
|
-
|
176
|
-
|
333
|
+
##
|
334
|
+
# @return [Integer] the right boundary of this node
|
335
|
+
#
|
336
|
+
def right
|
337
|
+
read_attribute(self.class.nested_set_options[:right])
|
177
338
|
end
|
178
339
|
|
340
|
+
##
|
341
|
+
# Caches the node as this node's parent.
|
342
|
+
#
|
179
343
|
def cache_parent(parent) #:nodoc:
|
180
344
|
self.parent = parent
|
181
345
|
end
|
182
346
|
|
347
|
+
##
|
348
|
+
# Caches the nodes as this node's children.
|
349
|
+
#
|
183
350
|
def cache_children(*nodes) #:nodoc:
|
184
351
|
@cached_children ||= []
|
185
|
-
@cached_children.push(*nodes)
|
186
|
-
end
|
187
|
-
|
188
|
-
def left
|
189
|
-
read_attribute(self.class.nested_set_options[:left])
|
190
|
-
end
|
191
|
-
|
192
|
-
def right
|
193
|
-
read_attribute(self.class.nested_set_options[:right])
|
352
|
+
children.target = @cached_children.push(*nodes)
|
194
353
|
end
|
195
|
-
|
196
|
-
|
354
|
+
|
355
|
+
##
|
356
|
+
# Rebuild this node's childrens boundaries
|
357
|
+
#
|
358
|
+
def recalculate_nested_set(left) #:nodoc:
|
197
359
|
child_left = left + 1
|
198
360
|
children.each do |child|
|
199
361
|
child_left = child.recalculate_nested_set(child_left)
|
@@ -208,7 +370,7 @@ module EvenBetterNestedSet
|
|
208
370
|
|
209
371
|
def illegal_nesting
|
210
372
|
if parent_id? and family_ids.include?(parent_id)
|
211
|
-
errors.add(:parent_id, 'cannot move node to its own descendant')
|
373
|
+
errors.add(:parent_id, I18n.t('even_better_nested_set.illegal_nesting', :default => 'cannot move node to its own descendant'))
|
212
374
|
end
|
213
375
|
end
|
214
376
|
|
@@ -293,57 +455,16 @@ module EvenBetterNestedSet
|
|
293
455
|
if self.class.nested_set_options[:scope] && parent_id
|
294
456
|
parent.reload # Make sure we are testing the record corresponding to the parent_id
|
295
457
|
if self.send(self.class.nested_set_options[:scope]) != parent.send(self.class.nested_set_options[:scope])
|
296
|
-
|
458
|
+
message = I18n.t('even_better_nested_set.parent_not_in_scope',
|
459
|
+
:default => "cannot be a record with a different {{scope_name}} to this record",
|
460
|
+
:scope_name => self.class.nested_set_options[:scope]
|
461
|
+
)
|
462
|
+
errors.add(:parent_id, message)
|
297
463
|
end
|
298
464
|
end
|
299
465
|
end
|
300
466
|
end
|
301
467
|
|
302
|
-
module ClassMethods
|
303
|
-
|
304
|
-
def acts_as_nested_set(options = {})
|
305
|
-
options = { :left => :left, :right => :right }.merge!(options)
|
306
|
-
options[:scope] = "#{options[:scope]}_id" if options[:scope]
|
307
|
-
|
308
|
-
include NestedSet
|
309
|
-
|
310
|
-
self.nested_set_options = options
|
311
|
-
|
312
|
-
class_eval <<-RUBY, __FILE__, __LINE__+1
|
313
|
-
def #{options[:left]}=(left)
|
314
|
-
raise EvenBetterNestedSet::IllegalAssignmentError, "#{options[:left]} is an internal attribute used by EvenBetterNestedSet, do not assign it directly as is may corrupt the data in your database"
|
315
|
-
end
|
316
|
-
|
317
|
-
def #{options[:right]}=(right)
|
318
|
-
raise EvenBetterNestedSet::IllegalAssignmentError, "#{options[:right]} is an internal attribute used by EvenBetterNestedSet, do not assign it directly as is may corrupt the data in your database"
|
319
|
-
end
|
320
|
-
RUBY
|
321
|
-
|
322
|
-
named_scope :roots, :conditions => { :parent_id => nil }, :order => "#{nested_set_column(:left)} asc"
|
323
|
-
|
324
|
-
has_many :uncached_children, :class_name => self.name, :foreign_key => :parent_id, :order => "#{nested_set_column(:left)} asc"
|
325
|
-
protected :uncached_children, :uncached_children=
|
326
|
-
|
327
|
-
belongs_to :parent, :class_name => self.name, :foreign_key => :parent_id
|
328
|
-
|
329
|
-
named_scope :descendants, lambda { |node|
|
330
|
-
left, right = find_boundaries(node.id)
|
331
|
-
{ :conditions => ["#{nested_set_column(:left)} > ? and #{nested_set_column(:right)} < ?", left, right],
|
332
|
-
:order => "#{nested_set_column(:left)} asc" }
|
333
|
-
}
|
334
|
-
|
335
|
-
before_create :append_node
|
336
|
-
before_update :move_node
|
337
|
-
before_destroy :reload
|
338
|
-
after_destroy :remove_node
|
339
|
-
validate_on_update :illegal_nesting
|
340
|
-
validate :validate_parent_is_within_scope
|
341
|
-
|
342
|
-
delegate :nested_set_column, :to => "self.class"
|
343
|
-
end
|
344
|
-
|
345
|
-
end
|
346
|
-
|
347
468
|
end
|
348
469
|
|
349
|
-
ActiveRecord::Base.
|
470
|
+
ActiveRecord::Base.extend EvenBetterNestedSet if defined?(ActiveRecord)
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: jnicklas-eb_nested_set
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.3.
|
4
|
+
version: 0.3.5
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Jonas Nicklas
|
@@ -9,7 +9,7 @@ autorequire: eb_nested_set
|
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
11
|
|
12
|
-
date: 2009-02-
|
12
|
+
date: 2009-02-16 00:00:00 -08:00
|
13
13
|
default_executable:
|
14
14
|
dependencies: []
|
15
15
|
|