Ruby on Rails | Screencasts | Download | Documentation | Weblog | Community | Source

Ticket #9516: remove_acts_as_nested_set.diff

File remove_acts_as_nested_set.diff, 19.3 kB (added by josh, 1 year ago)
  • activerecord/test/mixin_nested_set_test.rb

    old new  
    1 require 'abstract_unit' 
    2 require 'active_record/acts/nested_set' 
    3 require 'fixtures/mixin' 
    4 require 'pp' 
    5  
    6 class MixinNestedSetTest < Test::Unit::TestCase 
    7         fixtures :mixins 
    8                               
    9         def test_mixing_in_methods 
    10           ns = NestedSet.new 
    11                 assert( ns.respond_to?( :all_children ) ) 
    12                 assert_equal( ns.scope_condition, "root_id IS NULL" ) 
    13                  
    14                 check_method_mixins ns 
    15         end 
    16          
    17         def test_string_scope 
    18           ns = NestedSetWithStringScope.new 
    19            
    20           ns.root_id = 1 
    21           assert_equal( ns.scope_condition, "root_id = 1" ) 
    22           ns.root_id = 42 
    23           assert_equal( ns.scope_condition, "root_id = 42" ) 
    24           check_method_mixins ns 
    25   end 
    26    
    27   def test_symbol_scope 
    28     ns = NestedSetWithSymbolScope.new 
    29     ns.root_id = 1 
    30     assert_equal( ns.scope_condition, "root_id = 1" ) 
    31     ns.root_id = 42 
    32     assert_equal( ns.scope_condition, "root_id = 42" ) 
    33     check_method_mixins ns 
    34   end 
    35    
    36   def check_method_mixins( obj ) 
    37     [:scope_condition, :left_col_name, :right_col_name, :parent_column, :root?, :add_child, 
    38     :children_count, :full_set, :all_children, :direct_children].each { |symbol| assert( obj.respond_to?(symbol)) } 
    39   end 
    40  
    41   def set( id ) 
    42     NestedSet.find( 3000 + id ) 
    43   end 
    44    
    45   def test_adding_children 
    46     assert( set(1).unknown? ) 
    47     assert( set(2).unknown? ) 
    48     set(1).add_child set(2) 
    49      
    50     # Did we maintain adding the parent_ids? 
    51     assert( set(1).root? ) 
    52     assert( set(2).child? ) 
    53     assert( set(2).parent_id == set(1).id ) 
    54      
    55     # Check boundies 
    56     assert_equal( set(1).lft, 1 ) 
    57     assert_equal( set(2).lft, 2 ) 
    58     assert_equal( set(2).rgt, 3 ) 
    59     assert_equal( set(1).rgt, 4 ) 
    60      
    61     # Check children cound 
    62     assert_equal( set(1).children_count, 1 ) 
    63      
    64     set(1).add_child set(3) 
    65      
    66     #check boundries 
    67     assert_equal( set(1).lft, 1 ) 
    68     assert_equal( set(2).lft, 2 ) 
    69     assert_equal( set(2).rgt, 3 ) 
    70     assert_equal( set(3).lft, 4 ) 
    71     assert_equal( set(3).rgt, 5 ) 
    72     assert_equal( set(1).rgt, 6 ) 
    73      
    74     # How is the count looking? 
    75     assert_equal( set(1).children_count, 2 ) 
    76  
    77     set(2).add_child set(4) 
    78  
    79     # boundries 
    80     assert_equal( set(1).lft, 1 ) 
    81     assert_equal( set(2).lft, 2 ) 
    82     assert_equal( set(4).lft, 3 ) 
    83     assert_equal( set(4).rgt, 4 ) 
    84     assert_equal( set(2).rgt, 5 ) 
    85     assert_equal( set(3).lft, 6 ) 
    86     assert_equal( set(3).rgt, 7 ) 
    87     assert_equal( set(1).rgt, 8 ) 
    88      
    89     # Children count 
    90     assert_equal( set(1).children_count, 3 ) 
    91     assert_equal( set(2).children_count, 1 ) 
    92     assert_equal( set(3).children_count, 0 ) 
    93     assert_equal( set(4).children_count, 0 ) 
    94      
    95     set(2).add_child set(5) 
    96     set(4).add_child set(6) 
    97      
    98     assert_equal( set(2).children_count, 3 ) 
    99  
    100  
    101     # Children accessors 
    102     assert_equal( set(1).full_set.length, 6 ) 
    103     assert_equal( set(2).full_set.length, 4 ) 
    104     assert_equal( set(4).full_set.length, 2 ) 
    105      
    106     assert_equal( set(1).all_children.length, 5 ) 
    107     assert_equal( set(6).all_children.length, 0 ) 
    108      
    109     assert_equal( set(1).direct_children.length, 2 ) 
    110      
    111   end 
    112  
    113   def test_snipping_tree 
    114     big_tree = NestedSetWithStringScope.find( 4001 ) 
    115      
    116     # Make sure we have the right one 
    117     assert_equal( 3, big_tree.direct_children.length ) 
    118     assert_equal( 10, big_tree.full_set.length ) 
    119     assert_equal [4002, 4008, 4005], big_tree.direct_children.map(&:id) 
    120      
    121     NestedSetWithStringScope.find( 4005 ).destroy 
    122  
    123     big_tree = NestedSetWithStringScope.find( 4001 ) 
    124      
    125     assert_equal( 9, big_tree.full_set.length ) 
    126     assert_equal( 2, big_tree.direct_children.length ) 
    127      
    128     assert_equal( 1, NestedSetWithStringScope.find(4001).lft ) 
    129     assert_equal( 2, NestedSetWithStringScope.find(4002).lft ) 
    130     assert_equal( 3, NestedSetWithStringScope.find(4003).lft ) 
    131     assert_equal( 4, NestedSetWithStringScope.find(4003).rgt ) 
    132     assert_equal( 5, NestedSetWithStringScope.find(4004).lft ) 
    133     assert_equal( 6, NestedSetWithStringScope.find(4004).rgt ) 
    134     assert_equal( 7, NestedSetWithStringScope.find(4002).rgt ) 
    135     assert_equal( 8, NestedSetWithStringScope.find(4008).lft ) 
    136     assert_equal(15, NestedSetWithStringScope.find(4009).lft ) 
    137     assert_equal(16, NestedSetWithStringScope.find(4009).rgt ) 
    138     assert_equal(17, NestedSetWithStringScope.find(4010).lft ) 
    139     assert_equal(18, NestedSetWithStringScope.find(4010).rgt ) 
    140     assert_equal(19, NestedSetWithStringScope.find(4008).rgt ) 
    141     assert_equal(20, NestedSetWithStringScope.find(4001).rgt ) 
    142   end 
    143    
    144   def test_deleting_root 
    145     NestedSetWithStringScope.find(4001).destroy 
    146      
    147     assert( NestedSetWithStringScope.count == 0 ) 
    148   end             
    149                                 
    150   def test_common_usage 
    151     mixins(:set_1).add_child( mixins(:set_2) ) 
    152     assert_equal( 1, mixins(:set_1).direct_children.length ) 
    153  
    154     mixins(:set_2).add_child( mixins(:set_3) )                       
    155     assert_equal( 1, mixins(:set_1).direct_children.length )      
    156      
    157     # Local cache is now out of date! 
    158     # Problem: the update_alls update all objects up the tree 
    159     mixins(:set_1).reload 
    160     assert_equal( 2, mixins(:set_1).all_children.length )               
    161      
    162     assert_equal( 1, mixins(:set_1).lft ) 
    163     assert_equal( 2, mixins(:set_2).lft ) 
    164     assert_equal( 3, mixins(:set_3).lft ) 
    165     assert_equal( 4, mixins(:set_3).rgt ) 
    166     assert_equal( 5, mixins(:set_2).rgt ) 
    167     assert_equal( 6, mixins(:set_1).rgt ) 
    168            
    169     assert( mixins(:set_1).root? ) 
    170                    
    171     begin 
    172       mixins(:set_4).add_child( mixins(:set_1) ) 
    173       fail 
    174     rescue 
    175     end 
    176      
    177     assert_equal( 2, mixins(:set_1).all_children.length ) 
    178      
    179     mixins(:set_1).add_child mixins(:set_4) 
    180  
    181     assert_equal( 3, mixins(:set_1).all_children.length ) 
    182   end 
    183    
    184   def test_inheritance 
    185     parent = mixins(:sti_set_3100) 
    186     child = mixins(:sti_set_3101) 
    187     grandchild = mixins(:sti_set_3102) 
    188     assert_equal 5, parent.full_set.size 
    189     assert_equal 2, child.full_set.size 
    190     assert_equal 4, parent.all_children.size 
    191     assert_equal 1, child.all_children.size 
    192     assert_equal 2, parent.direct_children.size 
    193     assert_equal 1, child.direct_children.size 
    194     child.destroy 
    195     assert_equal 3, parent.full_set.size 
    196   end 
    197 end 
  • activerecord/test/fixtures/mixin.rb

    old new  
    3333 
    3434  def self.table_name() "mixins" end 
    3535end 
    36  
    37 class NestedSet < Mixin 
    38   acts_as_nested_set :scope => "root_id IS NULL" 
    39    
    40   def self.table_name() "mixins" end 
    41 end 
    42  
    43 class NestedSetWithStringScope < Mixin 
    44   acts_as_nested_set :scope => 'root_id = #{root_id}' 
    45    
    46   def self.table_name() "mixins" end 
    47 end 
    48  
    49 class NestedSetWithSymbolScope < Mixin 
    50   acts_as_nested_set :scope => :root 
    51    
    52   def self.table_name() "mixins" end 
    53 end 
    54  
    55 class NestedSetSuperclass < Mixin 
    56   acts_as_nested_set :scope => :root 
    57    
    58   def self.table_name() "mixins" end 
    59 end 
    60  
    61 class NestedSetSubclass < NestedSetSuperclass 
    62    
    63 end 
  • activerecord/test/fixtures/mixins.yml

    old new  
    7777  type: NestedSet 
    7878<% end %> 
    7979 
    80 # Nested set with STI 
    81 <% 
    82 [ [3100,    0, 1, 10, "NestedSetSuperclass"], 
    83   [3101, 3100, 2, 5, "NestedSetSubclass"], 
    84   [3102, 3101, 3, 4, "NestedSetSuperclass"], 
    85   [3103, 3100, 6, 9, "NestedSetSuperclass"], 
    86   [3104, 3103, 7, 8, "NestedSetSubclass"] 
    87 ].each do |sti| %> 
    88 sti_set_<%= sti[0] %>: 
    89   id: <%= sti[0] %> 
    90   parent_id: <%= sti[1] %> 
    91   lft: <%= sti[2] %> 
    92   rgt: <%= sti[3] %> 
    93   type: <%= sti[4] %> 
    94   root_id: 3100 
    95    
    96 <% end %> 
    97  
    9880# Big old set 
    9981<% 
    10082[[4001, 0, 1, 20], 
  • activerecord/lib/active_record/acts/nested_set.rb

    old new  
    1 module ActiveRecord 
    2   module Acts #:nodoc: 
    3     module NestedSet #:nodoc: 
    4       def self.included(base) 
    5         base.extend(ClassMethods)               
    6       end   
    7  
    8       # This +acts_as+ extension provides Nested Set functionality.  Nested Set is similiar to Tree, but with 
    9       # the added feature that you can select the children and all of their descendents with 
    10       # a single query.  A good use case for this is a threaded post system, where you want 
    11       # to display every reply to a comment without multiple selects. 
    12       # 
    13       # A Google search for "Nested Set" should point you to in the right direction to explain the 
    14       # database theory.  I figured out a bunch of this from 
    15       # http://threebit.net/tutorials/nestedset/tutorial1.html 
    16       # 
    17       # Instead of picturing a leaf node structure with children pointing back to their parent, 
    18       # the best way to imagine how this works is to think of the parent entity surrounding all 
    19       # of its children, and its parent surrounding it, etc.  Assuming that they are lined up 
    20       # horizontally, we store the left and right boundries in the database. 
    21       # 
    22       # Imagine: 
    23       #   root 
    24       #     |_ Child 1 
    25       #       |_ Child 1.1 
    26       #       |_ Child 1.2 
    27       #     |_ Child 2 
    28       #       |_ Child 2.1 
    29       #       |_ Child 2.2 
    30       # 
    31       # If my cirlces in circles description didn't make sense, check out this sweet 
    32       # ASCII art: 
    33       # 
    34       #     ___________________________________________________________________ 
    35       #    |  Root                                                             | 
    36       #    |    ____________________________    ____________________________   | 
    37       #    |   |  Child 1                  |   |  Child 2                  |   | 
    38       #    |   |   __________   _________  |   |   __________   _________  |   | 
    39       #    |   |  |  C 1.1  |  |  C 1.2 |  |   |  |  C 2.1  |  |  C 2.2 |  |   | 
    40       #    1   2  3_________4  5________6  7   8  9_________10 11_______12 13  14 
    41       #    |   |___________________________|   |___________________________|   | 
    42       #    |___________________________________________________________________|  
    43       # 
    44       # The numbers represent the left and right boundries.  The table then might 
    45       # look like this: 
    46       #    ID | PARENT | LEFT | RIGHT | DATA 
    47       #     1 |      0 |    1 |    14 | root 
    48       #     2 |      1 |    2 |     7 | Child 1 
    49       #     3 |      2 |    3 |     4 | Child 1.1 
    50       #     4 |      2 |    5 |     6 | Child 1.2 
    51       #     5 |      1 |    8 |    13 | Child 2 
    52       #     6 |      5 |    9 |    10 | Child 2.1 
    53       #     7 |      5 |   11 |    12 | Child 2.2 
    54       # 
    55       # So, to get all children of an entry, you 
    56       #     SELECT * WHERE CHILD.LEFT IS BETWEEN PARENT.LEFT AND PARENT.RIGHT 
    57       # 
    58       # To get the count, it's <tt>(LEFT - RIGHT + 1)/2</tt>, etc. 
    59       # 
    60       # To get the direct parent, it falls back to using the +PARENT_ID+ field.    
    61       # 
    62       # There are instance methods for all of these. 
    63       # 
    64       # The structure is good if you need to group things together; the downside is that 
    65       # keeping data integrity is a pain, and both adding and removing an entry 
    66       # require a full table write.         
    67       # 
    68       # This sets up a +before_destroy+ callback to prune the tree correctly if one of its 
    69       # elements gets deleted. 
    70       # 
    71       module ClassMethods                       
    72         # Configuration options are: 
    73         # 
    74         # * +parent_column+ - specifies the column name to use for keeping the position integer (default: +parent_id+) 
    75         # * +left_column+ - column name for left boundry data, default +lft+ 
    76         # * +right_column+ - column name for right boundry data, default +rgt+ 
    77         # * +scope+ - restricts what is to be considered a list. Given a symbol, it'll attach <tt>_id</tt>  
    78         #   (if it hasn't already been added) and use that as the foreign key restriction. It's also possible  
    79         #   to give it an entire string that is interpolated if you need a tighter scope than just a foreign key. 
    80         #   Example: <tt>acts_as_list :scope => 'todo_list_id = #{todo_list_id} AND completed = 0'</tt> 
    81         def acts_as_nested_set(options = {}) 
    82           configuration = { :parent_column => "parent_id", :left_column => "lft", :right_column => "rgt", :scope => "1 = 1" } 
    83            
    84           configuration.update(options) if options.is_a?(Hash) 
    85            
    86           configuration[:scope] = "#{configuration[:scope]}_id".intern if configuration[:scope].is_a?(Symbol) && configuration[:scope].to_s !~ /_id$/ 
    87            
    88           if configuration[:scope].is_a?(Symbol) 
    89             scope_condition_method = %( 
    90               def scope_condition 
    91                 if #{configuration[:scope].to_s}.nil? 
    92                   "#{configuration[:scope].to_s} IS NULL" 
    93                 else 
    94                   "#{configuration[:scope].to_s} = \#{#{configuration[:scope].to_s}}" 
    95                 end 
    96               end 
    97             ) 
    98           else 
    99             scope_condition_method = "def scope_condition() \"#{configuration[:scope]}\" end" 
    100           end 
    101          
    102           class_eval <<-EOV 
    103             include ActiveRecord::Acts::NestedSet::InstanceMethods 
    104  
    105             #{scope_condition_method} 
    106              
    107             def left_col_name() "#{configuration[:left_column]}" end 
    108  
    109             def right_col_name() "#{configuration[:right_column]}" end 
    110                
    111             def parent_column() "#{configuration[:parent_column]}" end 
    112  
    113           EOV 
    114         end 
    115       end 
    116        
    117       module InstanceMethods 
    118         # Returns +true+ is this is a root node.   
    119         def root? 
    120           parent_id = self[parent_column] 
    121           (parent_id == 0 || parent_id.nil?) && (self[left_col_name] == 1) && (self[right_col_name] > self[left_col_name]) 
    122         end                                                                                              
    123                                      
    124         # Returns +true+ is this is a child node 
    125         def child?                           
    126           parent_id = self[parent_column] 
    127           !(parent_id == 0 || parent_id.nil?) && (self[left_col_name] > 1) && (self[right_col_name] > self[left_col_name]) 
    128         end      
    129          
    130         # Returns +true+ if we have no idea what this is 
    131         def unknown? 
    132           !root? && !child? 
    133         end 
    134  
    135                       
    136         # Adds a child to this object in the tree.  If this object hasn't been initialized, 
    137         # it gets set up as a root node.  Otherwise, this method will update all of the 
    138         # other elements in the tree and shift them to the right, keeping everything 
    139         # balanced.  
    140         def add_child( child )      
    141           self.reload 
    142           child.reload 
    143  
    144           if child.root? 
    145             raise "Adding sub-tree isn\'t currently supported" 
    146           else 
    147             if ( (self[left_col_name] == nil) || (self[right_col_name] == nil) ) 
    148               # Looks like we're now the root node!  Woo 
    149               self[left_col_name] = 1 
    150               self[right_col_name] = 4 
    151                
    152               # What do to do about validation? 
    153               return nil unless self.save 
    154                
    155               child[parent_column] = self.id 
    156               child[left_col_name] = 2 
    157               child[right_col_name]= 3 
    158               return child.save 
    159             else 
    160               # OK, we need to add and shift everything else to the right 
    161               child[parent_column] = self.id 
    162               right_bound = self[right_col_name] 
    163               child[left_col_name] = right_bound 
    164               child[right_col_name] = right_bound + 1 
    165               self[right_col_name] += 2 
    166               self.class.base_class.transaction { 
    167                 self.class.base_class.update_all( "#{left_col_name} = (#{left_col_name} + 2)",  "#{scope_condition} AND #{left_col_name} >= #{right_bound}" ) 
    168                 self.class.base_class.update_all( "#{right_col_name} = (#{right_col_name} + 2)",  "#{scope_condition} AND #{right_col_name} >= #{right_bound}" ) 
    169                 self.save 
    170                 child.save 
    171               } 
    172             end 
    173           end                                    
    174         end 
    175                                     
    176         # Returns the number of nested children of this object. 
    177         def children_count 
    178           return (self[right_col_name] - self[left_col_name] - 1)/2 
    179         end 
    180                                                                 
    181         # Returns a set of itself and all of its nested children 
    182         def full_set 
    183           self.class.base_class.find(:all, :conditions => "#{scope_condition} AND (#{left_col_name} BETWEEN #{self[left_col_name]} and #{self[right_col_name]})" ) 
    184         end 
    185                    
    186         # Returns a set of all of its children and nested children 
    187         def all_children 
    188           self.class.base_class.find(:all, :conditions => "#{scope_condition} AND (#{left_col_name} > #{self[left_col_name]}) and (#{right_col_name} < #{self[right_col_name]})" ) 
    189         end 
    190                                    
    191         # Returns a set of only this entry's immediate children 
    192         def direct_children 
    193           self.class.base_class.find(:all, :conditions => "#{scope_condition} and #{parent_column} = #{self.id}", :order => left_col_name) 
    194         end 
    195                                        
    196         # Prunes a branch off of the tree, shifting all of the elements on the right 
    197         # back to the left so the counts still work. 
    198         def before_destroy 
    199           return if self[right_col_name].nil? || self[left_col_name].nil? 
    200           dif = self[right_col_name] - self[left_col_name] + 1 
    201  
    202           self.class.base_class.transaction { 
    203             self.class.base_class.delete_all( "#{scope_condition} and #{left_col_name} > #{self[left_col_name]} and #{right_col_name} < #{self[right_col_name]}" ) 
    204             self.class.base_class.update_all( "#{left_col_name} = (#{left_col_name} - #{dif})",  "#{scope_condition} AND #{left_col_name} >= #{self[right_col_name]}" ) 
    205             self.class.base_class.update_all( "#{right_col_name} = (#{right_col_name} - #{dif} )",  "#{scope_condition} AND #{right_col_name} >= #{self[right_col_name]}" ) 
    206           } 
    207         end 
    208       end 
    209     end 
    210   end 
    211 end 
  • activerecord/lib/active_record.rb

    old new  
    4545require 'active_record/timestamp' 
    4646require 'active_record/acts/list' 
    4747require 'active_record/acts/tree' 
    48 require 'active_record/acts/nested_set' 
    4948require 'active_record/locking/optimistic' 
    5049require 'active_record/locking/pessimistic' 
    5150require 'active_record/migration' 
     
    6766  include ActiveRecord::Reflection 
    6867  include ActiveRecord::Acts::Tree 
    6968  include ActiveRecord::Acts::List 
    70   include ActiveRecord::Acts::NestedSet 
    7169  include ActiveRecord::Calculations 
    7270  include ActiveRecord::XmlSerialization 
    7371  include ActiveRecord::AttributeMethods