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