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 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