eb_nested_set 0.3.3 → 0.3.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/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
|
|