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::TestCase7 fixtures :mixins8 9 def test_mixing_in_methods10 ns = NestedSet.new11 assert( ns.respond_to?( :all_children ) )12 assert_equal( ns.scope_condition, "root_id IS NULL" )13 14 check_method_mixins ns15 end16 17 def test_string_scope18 ns = NestedSetWithStringScope.new19 20 ns.root_id = 121 assert_equal( ns.scope_condition, "root_id = 1" )22 ns.root_id = 4223 assert_equal( ns.scope_condition, "root_id = 42" )24 check_method_mixins ns25 end26 27 def test_symbol_scope28 ns = NestedSetWithSymbolScope.new29 ns.root_id = 130 assert_equal( ns.scope_condition, "root_id = 1" )31 ns.root_id = 4232 assert_equal( ns.scope_condition, "root_id = 42" )33 check_method_mixins ns34 end35 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 end40 41 def set( id )42 NestedSet.find( 3000 + id )43 end44 45 def test_adding_children46 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 boundies56 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 cound62 assert_equal( set(1).children_count, 1 )63 64 set(1).add_child set(3)65 66 #check boundries67 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 # boundries80 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 count90 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 accessors102 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 end112 113 def test_snipping_tree114 big_tree = NestedSetWithStringScope.find( 4001 )115 116 # Make sure we have the right one117 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 ).destroy122 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 end143 144 def test_deleting_root145 NestedSetWithStringScope.find(4001).destroy146 147 assert( NestedSetWithStringScope.count == 0 )148 end149 150 def test_common_usage151 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 tree159 mixins(:set_1).reload160 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 begin172 mixins(:set_4).add_child( mixins(:set_1) )173 fail174 rescue175 end176 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 end183 184 def test_inheritance185 parent = mixins(:sti_set_3100)186 child = mixins(:sti_set_3101)187 grandchild = mixins(:sti_set_3102)188 assert_equal 5, parent.full_set.size189 assert_equal 2, child.full_set.size190 assert_equal 4, parent.all_children.size191 assert_equal 1, child.all_children.size192 assert_equal 2, parent.direct_children.size193 assert_equal 1, child.direct_children.size194 child.destroy195 assert_equal 3, parent.full_set.size196 end197 end -
activerecord/test/fixtures/mixin.rb
old new 33 33 34 34 def self.table_name() "mixins" end 35 35 end 36 37 class NestedSet < Mixin38 acts_as_nested_set :scope => "root_id IS NULL"39 40 def self.table_name() "mixins" end41 end42 43 class NestedSetWithStringScope < Mixin44 acts_as_nested_set :scope => 'root_id = #{root_id}'45 46 def self.table_name() "mixins" end47 end48 49 class NestedSetWithSymbolScope < Mixin50 acts_as_nested_set :scope => :root51 52 def self.table_name() "mixins" end53 end54 55 class NestedSetSuperclass < Mixin56 acts_as_nested_set :scope => :root57 58 def self.table_name() "mixins" end59 end60 61 class NestedSetSubclass < NestedSetSuperclass62 63 end -
activerecord/test/fixtures/mixins.yml
old new 77 77 type: NestedSet 78 78 <% end %> 79 79 80 # Nested set with STI81 <%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: 310095 96 <% end %>97 98 80 # Big old set 99 81 <% 100 82 [[4001, 0, 1, 20], -
activerecord/lib/active_record/acts/nested_set.rb
old new 1 module ActiveRecord2 module Acts #:nodoc:3 module NestedSet #:nodoc:4 def self.included(base)5 base.extend(ClassMethods)6 end7 8 # This +acts_as+ extension provides Nested Set functionality. Nested Set is similiar to Tree, but with9 # the added feature that you can select the children and all of their descendents with10 # a single query. A good use case for this is a threaded post system, where you want11 # 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 the14 # database theory. I figured out a bunch of this from15 # http://threebit.net/tutorials/nestedset/tutorial1.html16 #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 all19 # of its children, and its parent surrounding it, etc. Assuming that they are lined up20 # horizontally, we store the left and right boundries in the database.21 #22 # Imagine:23 # root24 # |_ Child 125 # |_ Child 1.126 # |_ Child 1.227 # |_ Child 228 # |_ Child 2.129 # |_ Child 2.230 #31 # If my cirlces in circles description didn't make sense, check out this sweet32 # 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 1441 # | |___________________________| |___________________________| |42 # |___________________________________________________________________|43 #44 # The numbers represent the left and right boundries. The table then might45 # look like this:46 # ID | PARENT | LEFT | RIGHT | DATA47 # 1 | 0 | 1 | 14 | root48 # 2 | 1 | 2 | 7 | Child 149 # 3 | 2 | 3 | 4 | Child 1.150 # 4 | 2 | 5 | 6 | Child 1.251 # 5 | 1 | 8 | 13 | Child 252 # 6 | 5 | 9 | 10 | Child 2.153 # 7 | 5 | 11 | 12 | Child 2.254 #55 # So, to get all children of an entry, you56 # SELECT * WHERE CHILD.LEFT IS BETWEEN PARENT.LEFT AND PARENT.RIGHT57 #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 that65 # keeping data integrity is a pain, and both adding and removing an entry66 # require a full table write.67 #68 # This sets up a +before_destroy+ callback to prune the tree correctly if one of its69 # elements gets deleted.70 #71 module ClassMethods72 # 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 possible79 # 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_condition91 if #{configuration[:scope].to_s}.nil?92 "#{configuration[:scope].to_s} IS NULL"93 else94 "#{configuration[:scope].to_s} = \#{#{configuration[:scope].to_s}}"95 end96 end97 )98 else99 scope_condition_method = "def scope_condition() \"#{configuration[:scope]}\" end"100 end101 102 class_eval <<-EOV103 include ActiveRecord::Acts::NestedSet::InstanceMethods104 105 #{scope_condition_method}106 107 def left_col_name() "#{configuration[:left_column]}" end108 109 def right_col_name() "#{configuration[:right_column]}" end110 111 def parent_column() "#{configuration[:parent_column]}" end112 113 EOV114 end115 end116 117 module InstanceMethods118 # 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 end123 124 # Returns +true+ is this is a child node125 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 end129 130 # Returns +true+ if we have no idea what this is131 def unknown?132 !root? && !child?133 end134 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 the138 # other elements in the tree and shift them to the right, keeping everything139 # balanced.140 def add_child( child )141 self.reload142 child.reload143 144 if child.root?145 raise "Adding sub-tree isn\'t currently supported"146 else147 if ( (self[left_col_name] == nil) || (self[right_col_name] == nil) )148 # Looks like we're now the root node! Woo149 self[left_col_name] = 1150 self[right_col_name] = 4151 152 # What do to do about validation?153 return nil unless self.save154 155 child[parent_column] = self.id156 child[left_col_name] = 2157 child[right_col_name]= 3158 return child.save159 else160 # OK, we need to add and shift everything else to the right161 child[parent_column] = self.id162 right_bound = self[right_col_name]163 child[left_col_name] = right_bound164 child[right_col_name] = right_bound + 1165 self[right_col_name] += 2166 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.save170 child.save171 }172 end173 end174 end175 176 # Returns the number of nested children of this object.177 def children_count178 return (self[right_col_name] - self[left_col_name] - 1)/2179 end180 181 # Returns a set of itself and all of its nested children182 def full_set183 self.class.base_class.find(:all, :conditions => "#{scope_condition} AND (#{left_col_name} BETWEEN #{self[left_col_name]} and #{self[right_col_name]})" )184 end185 186 # Returns a set of all of its children and nested children187 def all_children188 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 end190 191 # Returns a set of only this entry's immediate children192 def direct_children193 self.class.base_class.find(:all, :conditions => "#{scope_condition} and #{parent_column} = #{self.id}", :order => left_col_name)194 end195 196 # Prunes a branch off of the tree, shifting all of the elements on the right197 # back to the left so the counts still work.198 def before_destroy199 return if self[right_col_name].nil? || self[left_col_name].nil?200 dif = self[right_col_name] - self[left_col_name] + 1201 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 end208 end209 end210 end211 end -
activerecord/lib/active_record.rb
old new 45 45 require 'active_record/timestamp' 46 46 require 'active_record/acts/list' 47 47 require 'active_record/acts/tree' 48 require 'active_record/acts/nested_set'49 48 require 'active_record/locking/optimistic' 50 49 require 'active_record/locking/pessimistic' 51 50 require 'active_record/migration' … … 67 66 include ActiveRecord::Reflection 68 67 include ActiveRecord::Acts::Tree 69 68 include ActiveRecord::Acts::List 70 include ActiveRecord::Acts::NestedSet71 69 include ActiveRecord::Calculations 72 70 include ActiveRecord::XmlSerialization 73 71 include ActiveRecord::AttributeMethods