Ticket #8: better_nested_set.rb

File better_nested_set.rb, 23.5 kB (added by Tim Trautmann <timct@mac.com>, 2 years ago)

change of before_create method

Line 
1 module SymetrieCom
2   module Acts #:nodoc:
3     module NestedSet #:nodoc:
4       def self.append_features(base)
5         super       
6         base.extend(ClassMethods)             
7       end 
8
9       # better_nested_set ehances the core nested_set tree functionality provided in ruby_on_rails.
10       #
11       # This acts provides Nested Set functionality. Nested Set is a smart way to implement
12       # an _ordered_ tree, with the added feature that you can select the children and all of their
13       # descendents with a single query. The drawback is that insertion or move need some complex
14       # sql queries. But everything is done here by this module!
15       #
16       # Nested sets are appropriate each time you want either an orderd tree (menus,
17       # commercial categories) or an efficient way of querying big trees (threaded posts).
18       #
19       # == API
20       # Methods names are aligned on Tree's ones as much as possible, to make replacment from one
21       # by another easier, except for the creation:
22       #
23       # in acts_as_tree:
24       #   item.children.create(:name => "child1")
25       #
26       # in acts_as_nested_set:
27       #   # adds a new item at the "end" of the tree, i.e. with child.left = max(tree.right)+1
28       #   child = MyClass.new(:name => "child1")
29       #   child.save
30       #   # now move the item to its right place
31       #   child.move_to_child_of my_item
32       #
33       # You can use:
34       # * move_to_child_of
35       # * move_to_right_of
36       # * move_to_left_of
37       # and pass them an id or an object.
38       #
39       # Other methods added by this mixin are:
40       # * +root+ - root item of the tree (the one that has a nil parent; should have left_column = 1 too)
41       # * +roots+ - root items, in case of multiple roots (the ones that have a nil parent)
42       # * +level+ - number indicating the level, a root being level 0
43       # * +ancestors+ - array of all parents, with root as first item
44       # * +self_and_ancestors+ - array of all parents and self
45       # * +siblings+ - array of all siblings, that are the items sharing the same parent and level
46       # * +self_and_siblings+ - array of itself and all siblings
47       # * +children_count+ - count of all immediate children
48       # * +children+ - array of all immediate childrens
49       # * +all_children+ - array of all children and nested children
50       # * +full_set+ - array of itself and all children and nested children
51       #
52       # These should not be useful, except if you want to write direct SQL:
53       # * +left_col_name+ - name of the left column passed on the declaration line
54       # * +right_col_name+ - name of the right column passed on the declaration line   
55       # * +parent_col_name+ - name of the parent column passed on the declaration line
56       #
57       # recommandations:
58       # Don't name your left and right columns 'left' and 'right': these names are reserved on most of dbs.
59       # Usage is to name them 'lft' and 'rgt' for instance.
60       #
61       module ClassMethods               
62         # Configuration options are:
63         #
64         # * +parent_column+ - specifies the column name to use for keeping the position integer (default: parent_id)
65         # * +left_column+ - column name for left boundry data, default "lft"
66         # * +right_column+ - column name for right boundry data, default "rgt"
67         # * +text_column+ - column name for the title field (optional). Used as default in the
68         #   {your-class}_options_for_select helper method. If empty, will use the first string field
69         #   of your model class.
70         # * +scope+ - restricts what is to be considered a list. Given a symbol, it'll attach "_id"
71         #   (if that hasn't been already) and use that as the foreign key restriction. It's also possible
72         #   to give it an entire string that is interpolated if you need a tighter scope than just a foreign key.
73         #   Example: <tt>acts_as_nested_set :scope => 'todo_list_id = #{todo_list_id} AND completed = 0'</tt>
74         def acts_as_nested_set(options = {})         
75           if options[:scope].is_a?(Symbol)
76             if options[:scope].to_s !~ /_id$/
77               options[:scope] = "#{options[:scope]}_id".intern
78             end         
79             options[:scope] = %(#{options[:scope].to_s}.nil? ? "#{options[:scope].to_s} IS NULL" : "#{options[:scope].to_s} = \#{#{options[:scope].to_s}}")
80           end
81
82 #            options[:scope] = %("#{options[:scope]}")
83
84           write_inheritable_attribute(:acts_as_nested_set_options,
85              { :parent_column  => (options[:parent_column] || 'parent_id'),
86                :left_column    => (options[:left_column]   || 'lft'),
87                :right_column   => (options[:right_column]  || 'rgt'),
88                :scope          => (options[:scope] || '1 = 1'),
89                :text_column    => (options[:text_column] || columns.collect{|c| (c.type == :string) ? c.name : nil }.compact.first)
90               } )
91                
92           class_inheritable_reader :acts_as_nested_set_options
93        
94           # no bulk assignment
95           attr_protected  acts_as_nested_set_options[:left_column].intern,
96                           acts_as_nested_set_options[:right_column].intern,
97                           acts_as_nested_set_options[:parent_column].intern
98           # no assignment to structure fields
99           module_eval <<-"end_eval", __FILE__, __LINE__
100             def #{acts_as_nested_set_options[:left_column]}=(x)
101               raise ActiveRecord::ActiveRecordError, "Unauthorized assignment to #{acts_as_nested_set_options[:left_column]}: it's an internal field handled by acts_as_nested_set code, use move_to_* methods instead."
102             end
103             def #{acts_as_nested_set_options[:right_column]}=(x)
104               raise ActiveRecord::ActiveRecordError, "Unauthorized assignment to #{acts_as_nested_set_options[:right_column]}: it's an internal field handled by acts_as_nested_set code, use move_to_* methods instead."
105             end
106             def #{acts_as_nested_set_options[:parent_column]}=(x)
107               raise ActiveRecord::ActiveRecordError, "Unauthorized assignment to #{acts_as_nested_set_options[:parent_column]}: it's an internal field handled by acts_as_nested_set code, use move_to_* methods instead."
108             end
109           end_eval
110        
111           include SymetrieCom::Acts::NestedSet::InstanceMethods
112           extend SymetrieCom::Acts::NestedSet::ClassMethods
113          
114           # adds the helper for the class
115 #          ActionView::Base.send(:define_method, "#{Inflector.underscore(self.class)}_options_for_select") { special=nil
116 #              "#{acts_as_nested_set_options[:text_column]} || "#{self.class} id #{id}"
117 #            }
118          
119         end       
120
121         # Returns the single root
122         def root
123           self.find(:first, :conditions => "(#{acts_as_nested_set_options[:scope]} AND #{acts_as_nested_set_options[:parent_column]} IS NULL)")
124         end
125        
126         # Returns roots when multiple roots (or virtual root, which is the same)
127         def roots
128           self.find(:all, :conditions => "(#{acts_as_nested_set_options[:scope]} AND #{acts_as_nested_set_options[:parent_column]} IS NULL)", :order => "#{acts_as_nested_set_options[:left_column]}")
129         end                             
130       end
131      
132       module InstanceMethods
133         def left_col_name() acts_as_nested_set_options[:left_column] end
134         def right_col_name() acts_as_nested_set_options[:right_column] end             
135         def parent_col_name() acts_as_nested_set_options[:parent_column] end
136
137         # on creation, set automatically lft and rgt to the end of the tree
138         def before_create
139           maxright = self.class.maximum(acts_as_nested_set_options[:right_column], :conditions => acts_as_nested_set_options[:scope]) || 0
140           # adds the new node to the right of all existing nodes
141          
142           self[acts_as_nested_set_options[:left_column]] = maxright+1
143           self[acts_as_nested_set_options[:right_column]] = maxright+2
144         end
145
146         # Returns true if this is a root node.
147         def root?
148           parent_id = self[acts_as_nested_set_options[:parent_column]]
149           (parent_id == 0 || parent_id.nil?) && (self[acts_as_nested_set_options[:left_column]] == 1) && (self[acts_as_nested_set_options[:right_column]] > self[acts_as_nested_set_options[:left_column]])
150         end                                                                                             
151                                    
152         # Returns true is this is a child node
153         def child?                         
154           parent_id = self[acts_as_nested_set_options[:parent_column]]
155           !(parent_id == 0 || parent_id.nil?) && (self[acts_as_nested_set_options[:left_column]] > 1) && (self[acts_as_nested_set_options[:right_column]] > self[acts_as_nested_set_options[:left_column]])
156         end     
157        
158         # Returns true if we have no idea what this is
159         #
160         # Deprecated, will be removed in next versions
161         def unknown?
162           !root? && !child?
163         end
164        
165         # order by left column
166         def <=>(x)
167           self[acts_as_nested_set_options[:left_column]] <=> x[acts_as_nested_set_options[:left_column]]
168         end
169
170         # Adds a child to this object in the tree.  If this object hasn't been initialized,
171         # it gets set up as a root node.  Otherwise, this method will update all of the
172         # other elements in the tree and shift them to the right, keeping everything
173         # balanced.
174         #
175         # Deprecated, will be removed in next versions
176         def add_child( child )     
177           self.reload
178           child.reload
179
180           if child.root?
181             raise ActiveRecord::ActiveRecordError, "Adding sub-tree isn\'t currently supported"
182           else
183             if ( (self[acts_as_nested_set_options[:left_column]] == nil) || (self[acts_as_nested_set_options[:right_column]] == nil) )
184               # Looks like we're now the root node!  Woo
185               self[acts_as_nested_set_options[:left_column]] = 1
186               self[acts_as_nested_set_options[:right_column]] = 4
187              
188               # What do to do about validation?
189               return nil unless self.save
190              
191               child[acts_as_nested_set_options[:parent_column]] = self.id
192               child[acts_as_nested_set_options[:left_column]] = 2
193               child[acts_as_nested_set_options[:right_column]]= 3
194               return child.save
195             else
196               # OK, we need to add and shift everything else to the right
197               child[acts_as_nested_set_options[:parent_column]] = self.id
198               right_bound = self[acts_as_nested_set_options[:right_column]]
199               child[acts_as_nested_set_options[:left_column]] = right_bound
200               child[acts_as_nested_set_options[:right_column]] = right_bound + 1
201               self[acts_as_nested_set_options[:right_column]] += 2
202               self.class.transaction {
203                 self.class.update_all( "#{acts_as_nested_set_options[:left_column]} = (#{acts_as_nested_set_options[:left_column]} + 2)",  "#{acts_as_nested_set_options[:scope]} AND #{acts_as_nested_set_options[:left_column]} >= #{right_bound}" )
204                 self.class.update_all( "#{acts_as_nested_set_options[:right_column]} = (#{acts_as_nested_set_options[:right_column]} + 2)",  "#{acts_as_nested_set_options[:scope]} AND #{acts_as_nested_set_options[:right_column]} >= #{right_bound}" )
205                 self.save
206                 child.save
207               }
208             end
209           end                                   
210         end
211        
212         # Returns root
213         def root
214             self.class.find(:first, :conditions => "#{acts_as_nested_set_options[:scope]} AND (#{acts_as_nested_set_options[:parent_column]} IS NULL)")
215         end
216                
217         # Returns roots when multiple roots (or virtual root, which is the same)
218         def roots
219             self.class.find(:all, :conditions => "#{acts_as_nested_set_options[:scope]} AND (#{acts_as_nested_set_options[:parent_column]} IS NULL)", :order => "#{acts_as_nested_set_options[:left_column]}")
220         end
221                
222         # Returns the parent
223         def parent
224             self.class.find(self[acts_as_nested_set_options[:parent_column]]) if self[acts_as_nested_set_options[:parent_column]]
225         end
226        
227         # Returns an array of all parents
228         # Maybe 'full_outline' would be a better name, but we prefer to mimic the Tree class
229         def ancestors
230             self.class.find(:all, :conditions => "#{acts_as_nested_set_options[:scope]} AND (#{acts_as_nested_set_options[:left_column]} < #{self[acts_as_nested_set_options[:left_column]]} and #{acts_as_nested_set_options[:right_column]} > #{self[acts_as_nested_set_options[:right_column]]})", :order => acts_as_nested_set_options[:left_column] )
231         end
232        
233         # Returns the array of all parents and self
234         def self_and_ancestors
235             ancestors + [self]
236         end
237        
238         # Returns the array of all children of the parent, except self
239         def siblings
240             self_and_siblings - [self]
241         end
242        
243         # Returns the array of all children of the parent, included self
244         def self_and_siblings
245             if self[acts_as_nested_set_options[:parent_column]].nil? || self[acts_as_nested_set_options[:parent_column]].zero?
246                 [self]
247             else
248                 self.class.find(:all, :conditions => "#{acts_as_nested_set_options[:scope]} and #{acts_as_nested_set_options[:parent_column]} = #{self[acts_as_nested_set_options[:parent_column]]}", :order => acts_as_nested_set_options[:left_column])
249             end
250         end
251        
252         # Returns the level of this object in the tree
253         # root level is 0
254         def level
255             return 0 if self[acts_as_nested_set_options[:parent_column]].nil?
256             self.class.count("#{acts_as_nested_set_options[:scope]} AND (#{acts_as_nested_set_options[:left_column]} < #{self[acts_as_nested_set_options[:left_column]]} and #{acts_as_nested_set_options[:right_column]} > #{self[acts_as_nested_set_options[:right_column]]})")
257         end                                 
258                                            
259         # Returns the number of nested children of this object.
260         def children_count
261           return (self[acts_as_nested_set_options[:right_column]] - self[acts_as_nested_set_options[:left_column]] - 1)/2
262         end
263                                                                
264         # Returns a set of itself and all of its nested children
265         # Pass :exclude => item, or id, or [items or id] to exclude some parts of the tree
266         def full_set(special=nil)
267           return [self] if new_record? or self[acts_as_nested_set_options[:right_column]]-self[acts_as_nested_set_options[:left_column]] == 1
268 #          self.class.find(:all, :conditions => "#{acts_as_nested_set_options[:scope]} AND (#{acts_as_nested_set_options[:left_column]} BETWEEN #{self[acts_as_nested_set_options[:left_column]]} and #{self[acts_as_nested_set_options[:right_column]]})", :order => acts_as_nested_set_options[:left_column])
269           [self] + all_children(special)
270         end
271                  
272         # Returns a set of all of its children and nested children
273         # Pass :exclude => item, or id, or [items or id] to exclude some parts of the tree
274         def all_children(special=nil)
275           if special && special[:exclude]
276             transaction do
277               # exclude some items and all their children
278               special[:exclude] = [special[:exclude]] if !special[:exclude].is_a?(Array)
279               # get objects for ids
280               special[:exclude].collect! {|s| s.is_a?(self.class) ? s : self.class.find(s)}
281               # get all subtrees and flatten the list
282               exclude_list = special[:exclude].map{|e| e.full_set.map{|ee| ee.id}}.flatten.uniq.join(',')
283               if exclude_list.blank?
284                 self.class.find(:all, :conditions => "#{acts_as_nested_set_options[:scope]} AND (#{acts_as_nested_set_options[:left_column]} > #{self[acts_as_nested_set_options[:left_column]]}) and (#{acts_as_nested_set_options[:right_column]} < #{self[acts_as_nested_set_options[:right_column]]})", :order => acts_as_nested_set_options[:left_column])
285               else
286                 self.class.find(:all, :conditions => "#{acts_as_nested_set_options[:scope]} AND id NOT IN (#{exclude_list}) AND (#{acts_as_nested_set_options[:left_column]} > #{self[acts_as_nested_set_options[:left_column]]}) and (#{acts_as_nested_set_options[:right_column]} < #{self[acts_as_nested_set_options[:right_column]]})", :order => acts_as_nested_set_options[:left_column])
287               end
288             end
289           else
290             self.class.find(:all, :conditions => "#{acts_as_nested_set_options[:scope]} AND (#{acts_as_nested_set_options[:left_column]} > #{self[acts_as_nested_set_options[:left_column]]}) and (#{acts_as_nested_set_options[:right_column]} < #{self[acts_as_nested_set_options[:right_column]]})", :order => acts_as_nested_set_options[:left_column])
291           end
292         end
293
294         # Returns a set of only this entry's immediate children
295         def children
296             self.class.find(:all, :conditions => "#{acts_as_nested_set_options[:scope]} AND #{acts_as_nested_set_options[:parent_column]} = #{self.id}", :order => acts_as_nested_set_options[:left_column])
297         end
298                                      
299         # Prunes a branch off of the tree, shifting all of the elements on the right
300         # back to the left so the counts still work.
301         def before_destroy
302           return if self[acts_as_nested_set_options[:right_column]].nil? || self[acts_as_nested_set_options[:left_column]].nil?
303           dif = self[acts_as_nested_set_options[:right_column]] - self[acts_as_nested_set_options[:left_column]] + 1
304
305           self.class.transaction {
306             self.class.delete_all( "#{acts_as_nested_set_options[:scope]} AND #{acts_as_nested_set_options[:left_column]} > #{self[acts_as_nested_set_options[:left_column]]} and #{acts_as_nested_set_options[:right_column]} < #{self[acts_as_nested_set_options[:right_column]]}" )
307             self.class.update_all( "#{acts_as_nested_set_options[:left_column]} = (#{acts_as_nested_set_options[:left_column]} - #{dif})",  "#{acts_as_nested_set_options[:scope]} AND #{acts_as_nested_set_options[:left_column]} >= #{self[acts_as_nested_set_options[:right_column]]}" )
308             self.class.update_all( "#{acts_as_nested_set_options[:right_column]} = (#{acts_as_nested_set_options[:right_column]} - #{dif} )",  "#{acts_as_nested_set_options[:scope]} AND #{acts_as_nested_set_options[:right_column]} >= #{self[acts_as_nested_set_options[:right_column]]}" )
309           }
310         end
311        
312         # Move the node to the left of another node (you can pass id only)
313         def move_to_left_of(node)
314             self.move_to node, :left
315         end
316        
317         # Move the node to the left of another node (you can pass id only)
318         def move_to_right_of(node)
319             self.move_to node, :right
320         end
321        
322         # Move the node to the child of another node (you can pass id only)
323         def move_to_child_of(node)
324             self.move_to node, :child
325         end
326        
327         protected
328         def move_to(target, position)
329           raise ActiveRecord::ActiveRecordError, "You cannot move a new node" if self.id.nil?
330        
331           # use shorter names for readability: current left and right
332           cur_left, cur_right = self[acts_as_nested_set_options[:left_column]], self[acts_as_nested_set_options[:right_column]]
333              
334           # extent is the width of the tree self and children
335           extent = cur_right - cur_left + 1
336          
337           # load object if node is not an object
338           if !(self.class === target)
339             target = self.class.find(target)
340           end
341           target_left, target_right = target[acts_as_nested_set_options[:left_column]], target[acts_as_nested_set_options[:right_column]]
342
343           # detect impossible move
344           if ((cur_left <= target_left) && (target_left <= cur_right)) or ((cur_left <= target_right) && (target_right <= cur_right))
345             raise ActiveRecord::ActiveRecordError, "Impossible move, target node cannot be inside moved tree."
346           end
347        
348           # compute new left/right for self
349           if position == :child
350             if target_left < cur_left
351               new_left  = target_left + 1
352               new_right = target_left + extent
353             else
354               new_left  = target_left - extent + 1
355               new_right = target_left
356             end
357           elsif position == :left
358             if target_left < cur_left
359               new_left  = target_left
360               new_right = target_left + extent - 1
361             else
362               new_left  = target_left - extent
363               new_right = target_left - 1
364             end
365           elsif position == :right
366             if target_right < cur_right
367               new_left  = target_right + 1
368               new_right = target_right + extent
369             else
370               new_left  = target_right - extent + 1
371               new_right = target_right
372             end
373           else
374             raise ActiveRecord::ActiveRecordError, "Position should be either left or right ('#{position}' received)."
375           end
376          
377           # boundaries of update action
378           b_left, b_right = [cur_left, new_left].min, [cur_right, new_right].max
379          
380           # Shift value to move self to new position
381           shift = new_left - cur_left
382          
383           # Shift value to move nodes inside boundaries but not under self_and_children
384           updown = (shift > 0) ? -extent : extent
385          
386           # change nil to NULL for new parent
387           if position == :child
388             new_parent = target.id
389           else
390             new_parent = target[acts_as_nested_set_options[:parent_column]].nil? ? 'NULL' : target[acts_as_nested_set_options[:parent_column]]
391           end
392          
393           # update and that rules
394           self.class.update_all( "#{acts_as_nested_set_options[:left_column]} = CASE \
395                                       WHEN #{acts_as_nested_set_options[:left_column]} BETWEEN #{cur_left} AND #{cur_right} \
396                                         THEN #{acts_as_nested_set_options[:left_column]} + #{shift} \
397                                       WHEN #{acts_as_nested_set_options[:left_column]} BETWEEN #{b_left} AND #{b_right} \
398                                         THEN #{acts_as_nested_set_options[:left_column]} + #{updown} \
399                                       ELSE #{acts_as_nested_set_options[:left_column]} END, \
400                                   #{acts_as_nested_set_options[:right_column]} = CASE \
401                                       WHEN #{acts_as_nested_set_options[:right_column]} BETWEEN #{cur_left} AND #{cur_right} \
402                                         THEN #{acts_as_nested_set_options[:right_column]} + #{shift} \
403                                       WHEN #{acts_as_nested_set_options[:right_column]} BETWEEN #{b_left} AND #{b_right} \
404                                         THEN #{acts_as_nested_set_options[:right_column]} + #{updown} \
405                                       ELSE #{acts_as_nested_set_options[:right_column]} END, \
406                                   #{acts_as_nested_set_options[:parent_column]} = CASE \
407                                       WHEN #{self.class.primary_key} = #{self.id} \
408                                         THEN #{new_parent} \
409                                       ELSE #{acts_as_nested_set_options[:parent_column]} END",
410                                   acts_as_nested_set_options[:scope] )
411           self.reload
412         end
413        
414       end
415      
416     end
417   end
418 end