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

Ticket #9516: acts_as_nested_set_plugin.diff

File acts_as_nested_set_plugin.diff, 18.8 kB (added by josh, 1 year ago)
  • acts_as_nested_set/test/nested_set_test.rb

    old new  
     1$:.unshift File.dirname(__FILE__) + '/../lib' 
     2 
     3require 'test/unit' 
     4 
     5require 'rubygems' 
     6require 'active_record' 
     7 
     8require File.dirname(__FILE__) + '/../init' 
     9 
     10ActiveRecord::Base.establish_connection(:adapter => "sqlite3", :dbfile => ":memory:") 
     11 
     12class Mixin < ActiveRecord::Base 
     13end 
     14 
     15class NestedSet < Mixin 
     16  acts_as_nested_set :scope => "root_id IS NULL" 
     17 
     18  def self.table_name() "mixins" end 
     19end 
     20 
     21class NestedSetWithStringScope < Mixin 
     22  acts_as_nested_set :scope => 'root_id = #{root_id}' 
     23 
     24  def self.table_name() "mixins" end 
     25end 
     26 
     27class NestedSetWithSymbolScope < Mixin 
     28  acts_as_nested_set :scope => :root 
     29 
     30  def self.table_name() "mixins" end 
     31end 
     32 
     33class NestedSetSuperclass < Mixin 
     34  acts_as_nested_set :scope => :root 
     35 
     36  def self.table_name() "mixins" end 
     37end 
     38 
     39class NestedSetSubclass < NestedSetSuperclass 
     40end 
     41 
     42 
     43class MixinNestedSetTest < Test::Unit::TestCase 
     44 
     45  def setup 
     46    ActiveRecord::Schema.define(:version => 1) do 
     47      create_table :mixins do |t| 
     48        t.integer :pos, :parent_id, :lft, :rgt, :root_id 
     49        t.timestamps 
     50      end 
     51    end 
     52 
     53    (1..10).each { |counter| NestedSet.create! } 
     54 
     55    [ [0,  1, 10, NestedSetSuperclass], 
     56      [11, 2, 5, NestedSetSubclass], 
     57      [12, 3, 4, NestedSetSuperclass], 
     58      [11, 6, 9, NestedSetSuperclass], 
     59      [13, 7, 8, NestedSetSubclass] 
     60    ].each do |sti| 
     61      sti[3].create! :parent_id => sti[0], :lft => sti[1], :rgt => sti[2], :root_id => 3100 
     62    end 
     63 
     64    [ [0,  1, 20], 
     65      [16, 2, 7], 
     66      [17, 3, 4], 
     67      [17, 5, 6], 
     68      [16, 14, 13], 
     69      [20, 9, 10], 
     70      [20, 11, 12], 
     71      [16, 8, 19], 
     72      [23, 15, 16], 
     73      [23, 17, 18] 
     74    ].each do |set| 
     75      NestedSetWithStringScope.create! :parent_id => set[0], :lft => set[1], :rgt => set[2], :root_id => 42 
     76    end 
     77  end 
     78 
     79  def teardown 
     80    ActiveRecord::Base.connection.tables.each do |table| 
     81      ActiveRecord::Base.connection.drop_table(table) 
     82    end 
     83  end 
     84 
     85  def test_mixing_in_methods 
     86    ns = NestedSet.new 
     87    assert(ns.respond_to?(:all_children)) 
     88    assert_equal(ns.scope_condition, "root_id IS NULL") 
     89    check_method_mixins ns 
     90  end 
     91 
     92  def test_string_scope 
     93    ns = NestedSetWithStringScope.new 
     94    ns.root_id = 1 
     95    assert_equal(ns.scope_condition, "root_id = 1") 
     96    ns.root_id = 42 
     97    assert_equal(ns.scope_condition, "root_id = 42") 
     98    check_method_mixins ns 
     99  end 
     100 
     101  def test_symbol_scope 
     102    ns = NestedSetWithSymbolScope.new 
     103    ns.root_id = 1 
     104    assert_equal(ns.scope_condition, "root_id = 1") 
     105    ns.root_id = 42 
     106    assert_equal(ns.scope_condition, "root_id = 42") 
     107    check_method_mixins ns 
     108  end 
     109 
     110  def check_method_mixins(obj) 
     111    [:scope_condition, :left_col_name, :right_col_name, :parent_column, :root?, :add_child, 
     112    :children_count, :full_set, :all_children, :direct_children].each { |symbol| assert(obj.respond_to?(symbol)) } 
     113  end 
     114 
     115  def set(id) 
     116    NestedSet.find(id) 
     117  end 
     118 
     119  def test_adding_children 
     120    assert(set(1).unknown?) 
     121    assert(set(2).unknown?) 
     122    set(1).add_child set(2) 
     123 
     124    # Did we maintain adding the parent_ids? 
     125    assert(set(1).root?) 
     126    assert(set(2).child?) 
     127    assert(set(2).parent_id == set(1).id) 
     128 
     129    # Check boundies 
     130    assert_equal(set(1).lft, 1) 
     131    assert_equal(set(2).lft, 2) 
     132    assert_equal(set(2).rgt, 3) 
     133    assert_equal(set(1).rgt, 4) 
     134 
     135    # Check children cound 
     136    assert_equal(set(1).children_count, 1) 
     137 
     138    set(1).add_child set(3) 
     139 
     140    #check boundries 
     141    assert_equal(set(1).lft, 1) 
     142    assert_equal(set(2).lft, 2) 
     143    assert_equal(set(2).rgt, 3) 
     144    assert_equal(set(3).lft, 4) 
     145    assert_equal(set(3).rgt, 5) 
     146    assert_equal(set(1).rgt, 6) 
     147 
     148    # How is the count looking? 
     149    assert_equal(set(1).children_count, 2) 
     150 
     151    set(2).add_child set(4) 
     152 
     153    # boundries 
     154    assert_equal(set(1).lft, 1) 
     155    assert_equal(set(2).lft, 2) 
     156    assert_equal(set(4).lft, 3) 
     157    assert_equal(set(4).rgt, 4) 
     158    assert_equal(set(2).rgt, 5) 
     159    assert_equal(set(3).lft, 6) 
     160    assert_equal(set(3).rgt, 7) 
     161    assert_equal(set(1).rgt, 8) 
     162 
     163    # Children count 
     164    assert_equal(set(1).children_count, 3) 
     165    assert_equal(set(2).children_count, 1) 
     166    assert_equal(set(3).children_count, 0) 
     167    assert_equal(set(4).children_count, 0) 
     168 
     169    set(2).add_child set(5) 
     170    set(4).add_child set(6) 
     171 
     172    assert_equal(set(2).children_count, 3) 
     173 
     174 
     175    # Children accessors 
     176    assert_equal(set(1).full_set.length, 6) 
     177    assert_equal(set(2).full_set.length, 4) 
     178    assert_equal(set(4).full_set.length, 2) 
     179 
     180    assert_equal(set(1).all_children.length, 5) 
     181    assert_equal(set(6).all_children.length, 0) 
     182 
     183    assert_equal(set(1).direct_children.length, 2) 
     184 
     185  end 
     186 
     187   def test_snipping_tree 
     188     big_tree = NestedSetWithStringScope.find(16) 
     189 
     190     # Make sure we have the right one 
     191     assert_equal(3, big_tree.direct_children.length) 
     192     assert_equal(10, big_tree.full_set.length) 
     193     assert_equal [17, 23, 20], big_tree.direct_children.map(&:id) 
     194 
     195     NestedSetWithStringScope.find(20).destroy 
     196 
     197     big_tree = NestedSetWithStringScope.find(16) 
     198 
     199     assert_equal(9, big_tree.full_set.length) 
     200     assert_equal(2, big_tree.direct_children.length) 
     201 
     202     assert_equal(1, NestedSetWithStringScope.find(16).lft) 
     203     assert_equal(2, NestedSetWithStringScope.find(17).lft) 
     204     assert_equal(3, NestedSetWithStringScope.find(18).lft) 
     205     assert_equal(4, NestedSetWithStringScope.find(18).rgt) 
     206     assert_equal(5, NestedSetWithStringScope.find(19).lft) 
     207     assert_equal(6, NestedSetWithStringScope.find(19).rgt) 
     208     assert_equal(7, NestedSetWithStringScope.find(17).rgt) 
     209     assert_equal(8, NestedSetWithStringScope.find(23).lft) 
     210     assert_equal(15, NestedSetWithStringScope.find(24).lft) 
     211     assert_equal(16, NestedSetWithStringScope.find(24).rgt) 
     212     assert_equal(17, NestedSetWithStringScope.find(25).lft) 
     213     assert_equal(18, NestedSetWithStringScope.find(25).rgt) 
     214     assert_equal(19, NestedSetWithStringScope.find(23).rgt) 
     215     assert_equal(20, NestedSetWithStringScope.find(16).rgt) 
     216   end 
     217  
     218   def test_deleting_root 
     219     NestedSetWithStringScope.find(16).destroy 
     220     assert_equal 15, NestedSetWithStringScope.count 
     221   end 
     222 
     223   def test_common_usage 
     224     NestedSet.find(1).add_child(NestedSet.find(2)) 
     225     assert_equal(1, NestedSet.find(1).direct_children.length) 
     226 
     227     NestedSet.find(2).add_child(NestedSet.find(3)) 
     228     assert_equal(1, NestedSet.find(1).direct_children.length)  
     229 
     230     # Local cache is now out of date! 
     231     # Problem: the update_alls update all objects up the tree 
     232     assert_equal(2, NestedSet.find(1).all_children.length) 
     233 
     234     assert_equal(1, NestedSet.find(1).lft) 
     235     assert_equal(2, NestedSet.find(2).lft) 
     236     assert_equal(3, NestedSet.find(3).lft) 
     237     assert_equal(4, NestedSet.find(3).rgt) 
     238     assert_equal(5, NestedSet.find(2).rgt) 
     239     assert_equal(6, NestedSet.find(1).rgt) 
     240 
     241     assert(NestedSet.find(1).root?) 
     242 
     243     begin 
     244       NestedSet.find(4).add_child(NestedSet.find(1)) 
     245       fail 
     246     rescue 
     247     end 
     248 
     249     assert_equal(2, NestedSet.find(1).all_children.length) 
     250 
     251     NestedSet.find(1).add_child NestedSet.find(4) 
     252 
     253     assert_equal(3, NestedSet.find(1).all_children.length) 
     254   end 
     255  
     256   def test_inheritance 
     257     parent = NestedSetWithStringScope.find(11) 
     258     child = NestedSetWithStringScope.find(12) 
     259     grandchild = NestedSetWithStringScope.find(13) 
     260     assert_equal 5, parent.full_set.size 
     261     assert_equal 2, child.full_set.size 
     262     assert_equal 4, parent.all_children.size 
     263     assert_equal 1, child.all_children.size 
     264     assert_equal 2, parent.direct_children.size 
     265     assert_equal 1, child.direct_children.size 
     266     child.destroy 
     267     assert_equal 3, parent.full_set.size 
     268   end 
     269  
     270end 
  • acts_as_nested_set/init.rb

    old new  
  • acts_as_nested_set/lib/nested_set.rb

    old new  
     1module 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_nested_set :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        # Adds a child to this object in the tree.  If this object hasn't been initialized, 
     136        # it gets set up as a root node.  Otherwise, this method will update all of the 
     137        # other elements in the tree and shift them to the right, keeping everything 
     138        # balanced.  
     139        def add_child( child ) 
     140          self.reload 
     141          child.reload 
     142 
     143          if child.root? 
     144            raise "Adding sub-tree isn\'t currently supported" 
     145          else 
     146            if ( (self[left_col_name] == nil) || (self[right_col_name] == nil) ) 
     147              # Looks like we're now the root node!  Woo 
     148              self[left_col_name] = 1 
     149              self[right_col_name] = 4 
     150 
     151              # What do to do about validation? 
     152              return nil unless self.save 
     153 
     154              child[parent_column] = self.id 
     155              child[left_col_name] = 2 
     156              child[right_col_name]= 3 
     157              return child.save 
     158            else 
     159              # OK, we need to add and shift everything else to the right 
     160              child[parent_column] = self.id 
     161              right_bound = self[right_col_name] 
     162              child[left_col_name] = right_bound 
     163              child[right_col_name] = right_bound + 1 
     164              self[right_col_name] += 2 
     165              self.class.base_class.transaction { 
     166                self.class.base_class.update_all( "#{left_col_name} = (#{left_col_name} + 2)",  "#{scope_condition} AND #{left_col_name} >= #{right_bound}" ) 
     167                self.class.base_class.update_all( "#{right_col_name} = (#{right_col_name} + 2)",  "#{scope_condition} AND #{right_col_name} >= #{right_bound}" ) 
     168                self.save 
     169                child.save 
     170              } 
     171            end 
     172          end 
     173        end 
     174 
     175        # Returns the number of nested children of this object. 
     176        def children_count 
     177          return (self[right_col_name] - self[left_col_name] - 1)/2 
     178        end 
     179 
     180        # Returns a set of itself and all of its nested children 
     181        def full_set 
     182          self.class.base_class.find(:all, :conditions => "#{scope_condition} AND (#{left_col_name} BETWEEN #{self[left_col_name]} and #{self[right_col_name]})" ) 
     183        end 
     184 
     185        # Returns a set of all of its children and nested children 
     186        def all_children 
     187          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]})" ) 
     188        end 
     189 
     190        # Returns a set of only this entry's immediate children 
     191        def direct_children 
     192          self.class.base_class.find(:all, :conditions => "#{scope_condition} and #{parent_column} = #{self.id}", :order => left_col_name) 
     193        end 
     194 
     195        # Prunes a branch off of the tree, shifting all of the elements on the right 
     196        # back to the left so the counts still work. 
     197        def before_destroy 
     198          return if self[right_col_name].nil? || self[left_col_name].nil? 
     199          dif = self[right_col_name] - self[left_col_name] + 1 
     200 
     201          self.class.base_class.transaction { 
     202            self.class.base_class.delete_all( "#{scope_condition} and #{left_col_name} > #{self[left_col_name]} and #{right_col_name} < #{self[right_col_name]}" ) 
     203            self.class.base_class.update_all( "#{left_col_name} = (#{left_col_name} - #{dif})",  "#{scope_condition} AND #{left_col_name} >= #{self[right_col_name]}" ) 
     204            self.class.base_class.update_all( "#{right_col_name} = (#{right_col_name} - #{dif} )",  "#{scope_condition} AND #{right_col_name} >= #{self[right_col_name]}" ) 
     205          } 
     206        end 
     207      end 
     208    end 
     209  end 
     210end 
  • acts_as_nested_set/README

    old new  
     1ActsAsNestedSet 
     2============== 
     3 
     4This acts_as extension provides Nested Set functionality.  Nested Set is similiar to Tree, but with the added feature that you can select the children and all of their descendents with a single query.  A good use case for this is a threaded post system, where you want to display every reply to a comment without multiple selects. 
     5 
     6 
     7Example 
     8======= 
     9 
     10  class Product < ActiveRecord::Base 
     11    acts_as_nested_set 
     12  end 
     13 
     14 
     15Copyright (c) 2007 David Heinemeier Hansson, released under the MIT license