jnicklas-eb_nested_set 0.3.2 → 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.
Files changed (4) hide show
  1. data/README.md +33 -1
  2. data/Rakefile +6 -1
  3. data/lib/eb_nested_set.rb +193 -72
  4. 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.2"
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
- def root
101
- transaction do
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
- @root ||= base_class.roots.find(:first, :conditions => ["#{nested_set_column(:left)} <= ? AND #{nested_set_column(:right)} >= ?", left, right])
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
- def children
172
- @cached_children || uncached_children
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
- def children?
176
- children.empty?
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
- def recalculate_nested_set(left)
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
- errors.add(:parent_id, "cannot be a record with a different #{self.class.nested_set_options[:scope]} to this record")
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.send(:include, EvenBetterNestedSet) if defined?(ActiveRecord)
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.2
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 -08:00
12
+ date: 2009-02-16 00:00:00 -08:00
13
13
  default_executable:
14
14
  dependencies: []
15
15