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 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.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
- def root
101
- transaction do
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
- @root ||= base_class.roots.find(:first, :conditions => ["#{nested_set_column(:left)} <= ? AND #{nested_set_column(:right)} >= ?", left, right])
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
- def children
172
- @cached_children || uncached_children
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
- def children?
176
- children.empty?
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
- def recalculate_nested_set(left)
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
- errors.add(:parent_id, "cannot be a record with a different #{self.class.nested_set_options[:scope]} to this record")
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.send(:include, EvenBetterNestedSet) if defined?(ActiveRecord)
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.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-15 00:00:00 +01:00
12
+ date: 2009-04-07 00:00:00 +02:00
13
13
  default_executable:
14
14
  dependencies: []
15
15