diff options
author | David Heinemeier Hansson <david@loudthinking.com> | 2007-09-11 03:09:37 +0000 |
---|---|---|
committer | David Heinemeier Hansson <david@loudthinking.com> | 2007-09-11 03:09:37 +0000 |
commit | 11f4d28344e2b10de08a3dafdf1b0c5de34325c7 (patch) | |
tree | a35a3ada9b69d1c1ba5e71bf881143e4c1a8769f /activerecord | |
parent | bc290c083c6a8d8ec1b30de423c751f451f51d6f (diff) | |
download | rails-11f4d28344e2b10de08a3dafdf1b0c5de34325c7.tar.gz rails-11f4d28344e2b10de08a3dafdf1b0c5de34325c7.tar.bz2 rails-11f4d28344e2b10de08a3dafdf1b0c5de34325c7.zip |
Moved acts_as_nested_set into a plugin of the same name on the official Rails svn (closed #9516) [josh]
git-svn-id: http://svn-commit.rubyonrails.org/rails/trunk@7453 5ecf4fe2-1ee6-0310-87b1-e25e094e27de
Diffstat (limited to 'activerecord')
-rw-r--r-- | activerecord/CHANGELOG | 4 | ||||
-rwxr-xr-x | activerecord/lib/active_record.rb | 2 | ||||
-rw-r--r-- | activerecord/lib/active_record/acts/nested_set.rb | 211 | ||||
-rw-r--r-- | activerecord/test/fixtures/mixin.rb | 28 | ||||
-rw-r--r-- | activerecord/test/fixtures/mixins.yml | 18 | ||||
-rw-r--r-- | activerecord/test/mixin_nested_set_test.rb | 197 |
6 files changed, 30 insertions, 430 deletions
diff --git a/activerecord/CHANGELOG b/activerecord/CHANGELOG index 5ec2e389e0..b11138bc2f 100644 --- a/activerecord/CHANGELOG +++ b/activerecord/CHANGELOG @@ -1,8 +1,8 @@ *SVN* -* Remove acts_as_list. To use it, install the plugin. [josh, nzkoz] +* Moved acts_as_nested_set into a plugin of the same name on the official Rails svn #9516 [josh] - http://dev.rubyonrails.org/svn/rails/plugins/acts_as_list/ +* Moved acts_as_list into a plugin of the same name on the official Rails svn [josh] * Explicitly require active_record/query_cache before using it. [Jeremy Kemper] diff --git a/activerecord/lib/active_record.rb b/activerecord/lib/active_record.rb index 56481e9d93..c71afd5170 100755 --- a/activerecord/lib/active_record.rb +++ b/activerecord/lib/active_record.rb @@ -44,7 +44,6 @@ require 'active_record/aggregations' require 'active_record/transactions' require 'active_record/timestamp' require 'active_record/acts/tree' -require 'active_record/acts/nested_set' require 'active_record/locking/optimistic' require 'active_record/locking/pessimistic' require 'active_record/migration' @@ -65,7 +64,6 @@ ActiveRecord::Base.class_eval do include ActiveRecord::Transactions include ActiveRecord::Reflection include ActiveRecord::Acts::Tree - include ActiveRecord::Acts::NestedSet include ActiveRecord::Calculations include ActiveRecord::XmlSerialization include ActiveRecord::AttributeMethods diff --git a/activerecord/lib/active_record/acts/nested_set.rb b/activerecord/lib/active_record/acts/nested_set.rb index c983e1173c..e69de29bb2 100644 --- a/activerecord/lib/active_record/acts/nested_set.rb +++ b/activerecord/lib/active_record/acts/nested_set.rb @@ -1,211 +0,0 @@ -module ActiveRecord - module Acts #:nodoc: - module NestedSet #:nodoc: - def self.included(base) - base.extend(ClassMethods) - end - - # 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. - # - # A Google search for "Nested Set" should point you to in the right direction to explain the - # database theory. I figured out a bunch of this from - # http://threebit.net/tutorials/nestedset/tutorial1.html - # - # Instead of picturing a leaf node structure with children pointing back to their parent, - # the best way to imagine how this works is to think of the parent entity surrounding all - # of its children, and its parent surrounding it, etc. Assuming that they are lined up - # horizontally, we store the left and right boundries in the database. - # - # Imagine: - # root - # |_ Child 1 - # |_ Child 1.1 - # |_ Child 1.2 - # |_ Child 2 - # |_ Child 2.1 - # |_ Child 2.2 - # - # If my cirlces in circles description didn't make sense, check out this sweet - # ASCII art: - # - # ___________________________________________________________________ - # | Root | - # | ____________________________ ____________________________ | - # | | Child 1 | | Child 2 | | - # | | __________ _________ | | __________ _________ | | - # | | | C 1.1 | | C 1.2 | | | | C 2.1 | | C 2.2 | | | - # 1 2 3_________4 5________6 7 8 9_________10 11_______12 13 14 - # | |___________________________| |___________________________| | - # |___________________________________________________________________| - # - # The numbers represent the left and right boundries. The table then might - # look like this: - # ID | PARENT | LEFT | RIGHT | DATA - # 1 | 0 | 1 | 14 | root - # 2 | 1 | 2 | 7 | Child 1 - # 3 | 2 | 3 | 4 | Child 1.1 - # 4 | 2 | 5 | 6 | Child 1.2 - # 5 | 1 | 8 | 13 | Child 2 - # 6 | 5 | 9 | 10 | Child 2.1 - # 7 | 5 | 11 | 12 | Child 2.2 - # - # So, to get all children of an entry, you - # SELECT * WHERE CHILD.LEFT IS BETWEEN PARENT.LEFT AND PARENT.RIGHT - # - # To get the count, it's <tt>(LEFT - RIGHT + 1)/2</tt>, etc. - # - # To get the direct parent, it falls back to using the +PARENT_ID+ field. - # - # There are instance methods for all of these. - # - # The structure is good if you need to group things together; the downside is that - # keeping data integrity is a pain, and both adding and removing an entry - # require a full table write. - # - # This sets up a +before_destroy+ callback to prune the tree correctly if one of its - # elements gets deleted. - # - module ClassMethods - # Configuration options are: - # - # * +parent_column+ - specifies the column name to use for keeping the position integer (default: +parent_id+) - # * +left_column+ - column name for left boundry data, default +lft+ - # * +right_column+ - column name for right boundry data, default +rgt+ - # * +scope+ - restricts what is to be considered a list. Given a symbol, it'll attach <tt>_id</tt> - # (if it hasn't already been added) and use that as the foreign key restriction. It's also possible - # to give it an entire string that is interpolated if you need a tighter scope than just a foreign key. - # Example: <tt>acts_as_list :scope => 'todo_list_id = #{todo_list_id} AND completed = 0'</tt> - def acts_as_nested_set(options = {}) - configuration = { :parent_column => "parent_id", :left_column => "lft", :right_column => "rgt", :scope => "1 = 1" } - - configuration.update(options) if options.is_a?(Hash) - - configuration[:scope] = "#{configuration[:scope]}_id".intern if configuration[:scope].is_a?(Symbol) && configuration[:scope].to_s !~ /_id$/ - - if configuration[:scope].is_a?(Symbol) - scope_condition_method = %( - def scope_condition - if #{configuration[:scope].to_s}.nil? - "#{configuration[:scope].to_s} IS NULL" - else - "#{configuration[:scope].to_s} = \#{#{configuration[:scope].to_s}}" - end - end - ) - else - scope_condition_method = "def scope_condition() \"#{configuration[:scope]}\" end" - end - - class_eval <<-EOV - include ActiveRecord::Acts::NestedSet::InstanceMethods - - #{scope_condition_method} - - def left_col_name() "#{configuration[:left_column]}" end - - def right_col_name() "#{configuration[:right_column]}" end - - def parent_column() "#{configuration[:parent_column]}" end - - EOV - end - end - - module InstanceMethods - # Returns +true+ is this is a root node. - def root? - parent_id = self[parent_column] - (parent_id == 0 || parent_id.nil?) && (self[left_col_name] == 1) && (self[right_col_name] > self[left_col_name]) - end - - # Returns +true+ is this is a child node - def child? - parent_id = self[parent_column] - !(parent_id == 0 || parent_id.nil?) && (self[left_col_name] > 1) && (self[right_col_name] > self[left_col_name]) - end - - # Returns +true+ if we have no idea what this is - def unknown? - !root? && !child? - end - - - # Adds a child to this object in the tree. If this object hasn't been initialized, - # it gets set up as a root node. Otherwise, this method will update all of the - # other elements in the tree and shift them to the right, keeping everything - # balanced. - def add_child( child ) - self.reload - child.reload - - if child.root? - raise "Adding sub-tree isn\'t currently supported" - else - if ( (self[left_col_name] == nil) || (self[right_col_name] == nil) ) - # Looks like we're now the root node! Woo - self[left_col_name] = 1 - self[right_col_name] = 4 - - # What do to do about validation? - return nil unless self.save - - child[parent_column] = self.id - child[left_col_name] = 2 - child[right_col_name]= 3 - return child.save - else - # OK, we need to add and shift everything else to the right - child[parent_column] = self.id - right_bound = self[right_col_name] - child[left_col_name] = right_bound - child[right_col_name] = right_bound + 1 - self[right_col_name] += 2 - self.class.base_class.transaction { - self.class.base_class.update_all( "#{left_col_name} = (#{left_col_name} + 2)", "#{scope_condition} AND #{left_col_name} >= #{right_bound}" ) - self.class.base_class.update_all( "#{right_col_name} = (#{right_col_name} + 2)", "#{scope_condition} AND #{right_col_name} >= #{right_bound}" ) - self.save - child.save - } - end - end - end - - # Returns the number of nested children of this object. - def children_count - return (self[right_col_name] - self[left_col_name] - 1)/2 - end - - # Returns a set of itself and all of its nested children - def full_set - self.class.base_class.find(:all, :conditions => "#{scope_condition} AND (#{left_col_name} BETWEEN #{self[left_col_name]} and #{self[right_col_name]})" ) - end - - # Returns a set of all of its children and nested children - def all_children - 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]})" ) - end - - # Returns a set of only this entry's immediate children - def direct_children - self.class.base_class.find(:all, :conditions => "#{scope_condition} and #{parent_column} = #{self.id}", :order => left_col_name) - end - - # Prunes a branch off of the tree, shifting all of the elements on the right - # back to the left so the counts still work. - def before_destroy - return if self[right_col_name].nil? || self[left_col_name].nil? - dif = self[right_col_name] - self[left_col_name] + 1 - - self.class.base_class.transaction { - self.class.base_class.delete_all( "#{scope_condition} and #{left_col_name} > #{self[left_col_name]} and #{right_col_name} < #{self[right_col_name]}" ) - self.class.base_class.update_all( "#{left_col_name} = (#{left_col_name} - #{dif})", "#{scope_condition} AND #{left_col_name} >= #{self[right_col_name]}" ) - self.class.base_class.update_all( "#{right_col_name} = (#{right_col_name} - #{dif} )", "#{scope_condition} AND #{right_col_name} >= #{self[right_col_name]}" ) - } - end - end - end - end -end diff --git a/activerecord/test/fixtures/mixin.rb b/activerecord/test/fixtures/mixin.rb index 5e40c397c0..358dde1ae1 100644 --- a/activerecord/test/fixtures/mixin.rb +++ b/activerecord/test/fixtures/mixin.rb @@ -43,3 +43,31 @@ end class NestedSetSubclass < NestedSetSuperclass end + +class NestedSet < Mixin + acts_as_nested_set :scope => "root_id IS NULL" + + def self.table_name() "mixins" end +end + +class NestedSetWithStringScope < Mixin + acts_as_nested_set :scope => 'root_id = #{root_id}' + + def self.table_name() "mixins" end +end + +class NestedSetWithSymbolScope < Mixin + acts_as_nested_set :scope => :root + + def self.table_name() "mixins" end +end + +class NestedSetSuperclass < Mixin + acts_as_nested_set :scope => :root + + def self.table_name() "mixins" end +end + +class NestedSetSubclass < NestedSetSuperclass + +end diff --git a/activerecord/test/fixtures/mixins.yml b/activerecord/test/fixtures/mixins.yml index 6c66eb559c..c79ff41070 100644 --- a/activerecord/test/fixtures/mixins.yml +++ b/activerecord/test/fixtures/mixins.yml @@ -68,24 +68,6 @@ set_<%= counter %>: type: NestedSet <% end %> -# Nested set with STI -<% -[ [3100, 0, 1, 10, "NestedSetSuperclass"], - [3101, 3100, 2, 5, "NestedSetSubclass"], - [3102, 3101, 3, 4, "NestedSetSuperclass"], - [3103, 3100, 6, 9, "NestedSetSuperclass"], - [3104, 3103, 7, 8, "NestedSetSubclass"] -].each do |sti| %> -sti_set_<%= sti[0] %>: - id: <%= sti[0] %> - parent_id: <%= sti[1] %> - lft: <%= sti[2] %> - rgt: <%= sti[3] %> - type: <%= sti[4] %> - root_id: 3100 - -<% end %> - # Big old set <% [[4001, 0, 1, 20], diff --git a/activerecord/test/mixin_nested_set_test.rb b/activerecord/test/mixin_nested_set_test.rb index 87fa140dc9..e69de29bb2 100644 --- a/activerecord/test/mixin_nested_set_test.rb +++ b/activerecord/test/mixin_nested_set_test.rb @@ -1,197 +0,0 @@ -require 'abstract_unit' -require 'active_record/acts/nested_set' -require 'fixtures/mixin' -require 'pp' - -class MixinNestedSetTest < Test::Unit::TestCase - fixtures :mixins - - def test_mixing_in_methods - ns = NestedSet.new - assert( ns.respond_to?( :all_children ) ) - assert_equal( ns.scope_condition, "root_id IS NULL" ) - - check_method_mixins ns - end - - def test_string_scope - ns = NestedSetWithStringScope.new - - ns.root_id = 1 - assert_equal( ns.scope_condition, "root_id = 1" ) - ns.root_id = 42 - assert_equal( ns.scope_condition, "root_id = 42" ) - check_method_mixins ns - end - - def test_symbol_scope - ns = NestedSetWithSymbolScope.new - ns.root_id = 1 - assert_equal( ns.scope_condition, "root_id = 1" ) - ns.root_id = 42 - assert_equal( ns.scope_condition, "root_id = 42" ) - check_method_mixins ns - end - - def check_method_mixins( obj ) - [:scope_condition, :left_col_name, :right_col_name, :parent_column, :root?, :add_child, - :children_count, :full_set, :all_children, :direct_children].each { |symbol| assert( obj.respond_to?(symbol)) } - end - - def set( id ) - NestedSet.find( 3000 + id ) - end - - def test_adding_children - assert( set(1).unknown? ) - assert( set(2).unknown? ) - set(1).add_child set(2) - - # Did we maintain adding the parent_ids? - assert( set(1).root? ) - assert( set(2).child? ) - assert( set(2).parent_id == set(1).id ) - - # Check boundies - assert_equal( set(1).lft, 1 ) - assert_equal( set(2).lft, 2 ) - assert_equal( set(2).rgt, 3 ) - assert_equal( set(1).rgt, 4 ) - - # Check children cound - assert_equal( set(1).children_count, 1 ) - - set(1).add_child set(3) - - #check boundries - assert_equal( set(1).lft, 1 ) - assert_equal( set(2).lft, 2 ) - assert_equal( set(2).rgt, 3 ) - assert_equal( set(3).lft, 4 ) - assert_equal( set(3).rgt, 5 ) - assert_equal( set(1).rgt, 6 ) - - # How is the count looking? - assert_equal( set(1).children_count, 2 ) - - set(2).add_child set(4) - - # boundries - assert_equal( set(1).lft, 1 ) - assert_equal( set(2).lft, 2 ) - assert_equal( set(4).lft, 3 ) - assert_equal( set(4).rgt, 4 ) - assert_equal( set(2).rgt, 5 ) - assert_equal( set(3).lft, 6 ) - assert_equal( set(3).rgt, 7 ) - assert_equal( set(1).rgt, 8 ) - - # Children count - assert_equal( set(1).children_count, 3 ) - assert_equal( set(2).children_count, 1 ) - assert_equal( set(3).children_count, 0 ) - assert_equal( set(4).children_count, 0 ) - - set(2).add_child set(5) - set(4).add_child set(6) - - assert_equal( set(2).children_count, 3 ) - - - # Children accessors - assert_equal( set(1).full_set.length, 6 ) - assert_equal( set(2).full_set.length, 4 ) - assert_equal( set(4).full_set.length, 2 ) - - assert_equal( set(1).all_children.length, 5 ) - assert_equal( set(6).all_children.length, 0 ) - - assert_equal( set(1).direct_children.length, 2 ) - - end - - def test_snipping_tree - big_tree = NestedSetWithStringScope.find( 4001 ) - - # Make sure we have the right one - assert_equal( 3, big_tree.direct_children.length ) - assert_equal( 10, big_tree.full_set.length ) - assert_equal [4002, 4008, 4005], big_tree.direct_children.map(&:id) - - NestedSetWithStringScope.find( 4005 ).destroy - - big_tree = NestedSetWithStringScope.find( 4001 ) - - assert_equal( 9, big_tree.full_set.length ) - assert_equal( 2, big_tree.direct_children.length ) - - assert_equal( 1, NestedSetWithStringScope.find(4001).lft ) - assert_equal( 2, NestedSetWithStringScope.find(4002).lft ) - assert_equal( 3, NestedSetWithStringScope.find(4003).lft ) - assert_equal( 4, NestedSetWithStringScope.find(4003).rgt ) - assert_equal( 5, NestedSetWithStringScope.find(4004).lft ) - assert_equal( 6, NestedSetWithStringScope.find(4004).rgt ) - assert_equal( 7, NestedSetWithStringScope.find(4002).rgt ) - assert_equal( 8, NestedSetWithStringScope.find(4008).lft ) - assert_equal(15, NestedSetWithStringScope.find(4009).lft ) - assert_equal(16, NestedSetWithStringScope.find(4009).rgt ) - assert_equal(17, NestedSetWithStringScope.find(4010).lft ) - assert_equal(18, NestedSetWithStringScope.find(4010).rgt ) - assert_equal(19, NestedSetWithStringScope.find(4008).rgt ) - assert_equal(20, NestedSetWithStringScope.find(4001).rgt ) - end - - def test_deleting_root - NestedSetWithStringScope.find(4001).destroy - - assert( NestedSetWithStringScope.count == 0 ) - end - - def test_common_usage - mixins(:set_1).add_child( mixins(:set_2) ) - assert_equal( 1, mixins(:set_1).direct_children.length ) - - mixins(:set_2).add_child( mixins(:set_3) ) - assert_equal( 1, mixins(:set_1).direct_children.length ) - - # Local cache is now out of date! - # Problem: the update_alls update all objects up the tree - mixins(:set_1).reload - assert_equal( 2, mixins(:set_1).all_children.length ) - - assert_equal( 1, mixins(:set_1).lft ) - assert_equal( 2, mixins(:set_2).lft ) - assert_equal( 3, mixins(:set_3).lft ) - assert_equal( 4, mixins(:set_3).rgt ) - assert_equal( 5, mixins(:set_2).rgt ) - assert_equal( 6, mixins(:set_1).rgt ) - - assert( mixins(:set_1).root? ) - - begin - mixins(:set_4).add_child( mixins(:set_1) ) - fail - rescue - end - - assert_equal( 2, mixins(:set_1).all_children.length ) - - mixins(:set_1).add_child mixins(:set_4) - - assert_equal( 3, mixins(:set_1).all_children.length ) - end - - def test_inheritance - parent = mixins(:sti_set_3100) - child = mixins(:sti_set_3101) - grandchild = mixins(:sti_set_3102) - assert_equal 5, parent.full_set.size - assert_equal 2, child.full_set.size - assert_equal 4, parent.all_children.size - assert_equal 1, child.all_children.size - assert_equal 2, parent.direct_children.size - assert_equal 1, child.direct_children.size - child.destroy - assert_equal 3, parent.full_set.size - end -end |