jnicklas-eb_nested_set 0.3.2 → 0.3.5

Sign up to get free protection for your applications and to get access to all the features.
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