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 3 require 'test/unit' 4 5 require 'rubygems' 6 require 'active_record' 7 8 require File.dirname(__FILE__) + '/../init' 9 10 ActiveRecord::Base.establish_connection(:adapter => "sqlite3", :dbfile => ":memory:") 11 12 class Mixin < ActiveRecord::Base 13 end 14 15 class NestedSet < Mixin 16 acts_as_nested_set :scope => "root_id IS NULL" 17 18 def self.table_name() "mixins" end 19 end 20 21 class NestedSetWithStringScope < Mixin 22 acts_as_nested_set :scope => 'root_id = #{root_id}' 23 24 def self.table_name() "mixins" end 25 end 26 27 class NestedSetWithSymbolScope < Mixin 28 acts_as_nested_set :scope => :root 29 30 def self.table_name() "mixins" end 31 end 32 33 class NestedSetSuperclass < Mixin 34 acts_as_nested_set :scope => :root 35 36 def self.table_name() "mixins" end 37 end 38 39 class NestedSetSubclass < NestedSetSuperclass 40 end 41 42 43 class 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 270 end -
acts_as_nested_set/init.rb
old new -
acts_as_nested_set/lib/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_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 210 end -
acts_as_nested_set/README
old new 1 ActsAsNestedSet 2 ============== 3 4 This 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 7 Example 8 ======= 9 10 class Product < ActiveRecord::Base 11 acts_as_nested_set 12 end 13 14 15 Copyright (c) 2007 David Heinemeier Hansson, released under the MIT license