Skip to content
Browse files

code from existing svn repo

  • Loading branch information...
0 parents commit 91a7fb680353b1dc832cdfe69dba60909b14a34b @ffmike committed
Showing with 1,073 additions and 0 deletions.
  1. +14 −0 CHANGELOG
  2. +137 −0 README
  3. +24 −0 Rakefile
  4. +2 −0 init.rb
  5. +446 −0 lib/acts_as_ordered_tree.rb
  6. +3 −0 lib/person.rb
  7. +15 −0 test/abstract_unit.rb
  8. +413 −0 test/acts_as_ordered_tree_test.rb
  9. +11 −0 test/database.yml
  10. +8 −0 test/schema.rb
14 CHANGELOG
@@ -0,0 +1,14 @@
+
+v.1.2 - Wed May 17 2007
+ - renamed syblings to siblings
+ - renamed self_and_syblings to self_and_siblings
+ - some other minor code changes
+ - updated tests
+
+ Note:
+ I attempted to rewrite the tests using fixtures,
+ but some of the tests were failing.
+ I presume this is due to fixtures not supporting
+ nested transactions. The results of this effort
+ can be found in the fixtures_not_working/ branch.
+
137 README
@@ -0,0 +1,137 @@
+= ActsAsOrderedTree
+ v.1.2
+ +----+-----------+----------+---------+
+ node_1 | id | parent_id | position | name |
+ \_ node_2 +----+-----------+----------+---------+
+ \_ node_3 | 1 | 0 | 1 | Node_1 |
+ | \_ node_4 | 2 | 1 | 1 | Node_2 |
+ | \_ node_5 | 3 | 1 | 2 | Node_3 |
+ | | \_ node_8 | 4 | 3 | 1 | Node_4 |
+ | | \_ node_9 | 5 | 3 | 2 | Node_5 |
+ | \_ node_10 | 6 | 1 | 3 | Node_6 |
+ | \_ node_11 | 7 | 1 | 4 | Node_7 |
+ \_ node_6 | 8 | 5 | 1 | Node_8 |
+ \_ node_7 | 9 | 5 | 2 | Node_9 |
+ | | 10 | 3 | 3 | Node_10 |
+ | | 11 | 3 | 4 | Node_11 |
+ node_12 | 12 | 0 | 2 | Node_12 |
+ \_ node_13 | 13 | 12 | 1 | Node_13 |
+ \_ node_14 | 14 | 12 | 2 | Node_14 |
+ | \_ node_15 | 15 | 14 | 1 | Node_15 |
+ | \_ node_16 | 16 | 14 | 2 | Node_16 |
+ | | \_ node_19 | 17 | 12 | 3 | Node_17 |
+ | | \_ node_20 | 18 | 12 | 4 | Node_18 |
+ | \_ node_21 | 19 | 16 | 1 | Node_19 |
+ | \_ node_22 | 20 | 16 | 2 | Node_20 |
+ \_ node_17 | 21 | 14 | 3 | Node_21 |
+ \_ node_18 | 22 | 14 | 4 | Node_22 |
+ +----+-----------+----------+---------+
+
+
+ class Person < ActiveRecord::Base
+ acts_as_ordered_tree :foreign_key => :parent_id,
+ :order => :position
+ end
+
+ class CreatePeople < ActiveRecord::Migration
+ def self.up
+ create_table :people do |t|
+ t.column :parent_id ,:integer ,:null => false ,:default => 0
+ t.column :position ,:integer
+ end
+ add_index(:people, :parent_id)
+ end
+ end
+
+ Which "in effect" sets up the following:
+
+ belongs_to :parent,
+ :class_name => Person,
+ :foreign_key => :parent_id
+
+ has_many :children,
+ :class_name => Person,
+ :foreign_key => :parent_id,
+ :order => :position
+
+
+
+= Overview
+
+ Actions Tree Methods List Method
+ --------------------------------------------------------------------------------------------------
+ Create
+ To create a child object at a specific position,
+ use one of the following:
+ Person.create(:parent_id => parent.id, :position => 2)
+ parent.children << Person.new(:position => 3)
+ parent.children.create(:position => 5)
+
+ To create a new 'root', use:
+ Person.create(:position => 2)
+
+ :position will default to the bottom of the parent's list
+ :parent_id defaults to 0 (Class.roots)
+
+ Read
+ roots (class method) self_and_siblings
+
+ root siblings
+
+ parent position_in_list
+
+ ancestors
+
+ children
+
+ descendants
+
+ Update
+ shift_to(parent = nil, position = nil) move_above(sibling = nil)
+
+ orphan move_higher
+
+ orphan_children move_lower
+
+ parent_adopts_children move_to_top
+
+ orphan_self_and_children move_to_bottom
+
+ orphan_self_and_parent_adopts_children
+
+ Destroy
+ destroy (deletes all descendants)
+
+ destroy_and_orphan_children
+
+ destroy_and_parent_adopts_children
+
+= Install
+
+ ./script/plugin install svn://rubyforge.org/var/svn/ordered-tree/acts_as_ordered_tree
+
+ To test, run 'rake test:plugins', or 'rake' from the plugin's directory.
+
+ There is a drag-n-drop demo located at svn://rubyforge.org/var/svn/ordered-tree/demo
+
+= License
+
+Copyright (c) 2006,2007 Brian D. Burns <wizard.rb@gmail.com>
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+the Software, and to permit persons to whom the Software is furnished to do so,
+subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
24 Rakefile
@@ -0,0 +1,24 @@
+require 'rake'
+require 'rake/testtask'
+require 'rake/clean'
+require 'rake/rdoctask'
+
+desc 'Default: run acts_as_ordered_tree unit tests.'
+task :default => :test
+
+desc 'Test the acts_as_ordered_tree plugin.'
+Rake::TestTask.new(:test) do |t|
+ t.libs << 'lib'
+ t.pattern = 'test/**/*_test.rb'
+ t.verbose = true
+end
+
+desc 'Generate documentation for the acts_as_ordered_tree plugin.'
+Rake::RDocTask.new(:rdoc) do |rdoc|
+ rdoc.rdoc_dir = 'rdoc'
+ rdoc.title = 'ActsAsOrderedTree'
+ rdoc.options << '--line-numbers' << '--inline-source'
+ rdoc.rdoc_files.include('README')
+ rdoc.rdoc_files.include('lib/**/*.rb')
+ rdoc.rdoc_files.exclude('lib/person.rb')
+end
2 init.rb
@@ -0,0 +1,2 @@
+require File.dirname(__FILE__) + '/lib/acts_as_ordered_tree'
+ActiveRecord::Base.send(:include, WizardActsAsOrderedTree::Acts::OrderedTree)
446 lib/acts_as_ordered_tree.rb
@@ -0,0 +1,446 @@
+# Acts As Ordered Tree v.1.2
+# Copyright (c) 2006 Brian D. Burns <wizard.rb@gmail.com>
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy of
+# this software and associated documentation files (the "Software"), to deal in
+# the Software without restriction, including without limitation the rights to
+# use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+# the Software, and to permit persons to whom the Software is furnished to do so,
+# subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in all
+# copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+# FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+# COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+# IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+module WizardActsAsOrderedTree #:nodoc:
+ module Acts #:nodoc:
+ module OrderedTree #:nodoc:
+ def self.included(base)
+ base.extend AddActsAsMethod
+ end
+
+ module AddActsAsMethod
+ # Configuration:
+ #
+ # class Person < ActiveRecord::Base
+ # acts_as_ordered_tree :foreign_key => :parent_id,
+ # :order => :position
+ # end
+ #
+ # class CreatePeople < ActiveRecord::Migration
+ # def self.up
+ # create_table :people do |t|
+ # t.column :parent_id ,:integer ,:null => false ,:default => 0
+ # t.column :position ,:integer
+ # end
+ # add_index(:people, :parent_id)
+ # end
+ # end
+ #
+ #
+ def acts_as_ordered_tree(options = {})
+ configuration = { :foreign_key => :parent_id ,
+ :order => :position }
+ configuration.update(options) if options.is_a?(Hash)
+
+ belongs_to :parent_node,
+ :class_name => name,
+ :foreign_key => configuration[:foreign_key]
+
+ has_many :child_nodes,
+ :class_name => name,
+ :foreign_key => configuration[:foreign_key],
+ :order => configuration[:order]
+
+ cattr_reader :roots
+
+ class_eval <<-EOV
+ include WizardActsAsOrderedTree::Acts::OrderedTree::InstanceMethods
+
+ def foreign_key_column
+ :'#{configuration[:foreign_key]}'
+ end
+
+ def order_column
+ :'#{configuration[:order]}'
+ end
+
+ def self.roots(reload = false)
+ reload = true if !@@roots
+ reload ? find(:all, :conditions => "#{configuration[:foreign_key]} = 0", :order => "#{configuration[:order]}") : @@roots
+ end
+
+ before_create :add_to_list
+ before_update :check_list_changes
+ after_update :reorder_old_list
+ before_destroy :destroy_descendants
+ after_destroy :reorder_old_list
+ EOV
+ end #acts_as_ordered_tree
+ end #module AddActsAsMethod
+
+ module InstanceMethods
+ ## Tree Read Methods
+
+ # returns an ordered array of all nodes without a parent
+ # i.e. parent_id = 0
+ #
+ # return is cached
+ # use self.class.roots(true) to force a reload
+ def self.roots(reload = false)
+ # stub for rdoc - overwritten in AddActsAsMethod::acts_as_ordered_tree
+ end
+
+ # returns the top node in the object's tree
+ #
+ # return is cached, unless nil
+ # use root(true) to force a reload
+ def root(reload = false)
+ reload = true if !@root
+ reload ? find_root : @root
+ end
+
+ # returns an array of ancestors, starting from parent until root.
+ # return is cached
+ # use ancestors(true) to force a reload
+ def ancestors(reload = false)
+ reload = true if !@ancestors
+ reload ? find_ancestors : @ancestors
+ end
+
+ # returns object's parent in the tree
+ # auto-loads itself on first access
+ # instead of returning "<parent_node not loaded yet>"
+ #
+ # return is cached, unless nil
+ # use parent(true) to force a reload
+ def parent(reload=false)
+ reload = true if !@parent
+ reload ? parent_node(true) : @parent
+ end
+
+ # returns an array of the object's immediate children
+ # auto-loads itself on first access
+ # instead of returning "<child_nodes not loaded yet>"
+ #
+ # return is cached
+ # use children(true) to force a reload
+ def children(reload=false)
+ reload = true if !@children
+ reload ? child_nodes(true) : @children
+ end
+
+ # returns an array of the object's descendants
+ #
+ # return is cached
+ # use descendants(true) to force a reload
+ def descendants(reload = false)
+ @descendants = nil if reload
+ reload = true if !@descendants
+ reload ? find_descendants(self) : @descendants
+ end
+
+ ## List Read Methods
+
+ # returns an array of the object's siblings, including itself
+ #
+ # return is cached
+ # use self_and_siblings(true) to force a reload
+ def self_and_siblings(reload = false)
+ parent(reload) ? parent.children(reload) : self.class.roots(reload)
+ end
+
+ # returns an array of the object's siblings, excluding itself
+ #
+ # return is cached
+ # use siblings(true) to force a reload
+ def siblings(reload = false)
+ self_and_siblings(reload) - [self]
+ end
+
+ def next_sibling(reload = false)
+ sib = self_and_siblings(reload)
+ if self.position_in_list >= sib.count
+ next_sibling = sib[0]
+ else
+ next_sibling = sib[self.position_in_list]
+ end
+ end
+
+ # returns object's position in the list
+ # the list will either be parent.children,
+ # or self.class.roots
+ #
+ # i.e. self.position
+ def position_in_list
+ self[order_column]
+ end
+
+ ## Tree Update Methods
+
+ # shifts a node to another parent, optionally specifying it's position
+ # (descendants will follow along)
+ #
+ # shift_to()
+ # defaults to the bottom of the "roots" list
+ #
+ # shift_to(nil, new_sibling)
+ # will move the item to "roots",
+ # and position the item above new_sibling
+ #
+ # shift_to(new_parent)
+ # will move the item to the new parent,
+ # and position at the bottom of the parent's list
+ #
+ # shift_to(new_parent, new_sibling)
+ # will move the item to the new parent,
+ # and position the item above new_sibling
+ #
+ def shift_to(new_parent = nil, new_sibling = nil)
+ if new_parent
+ ok = new_parent.children(true) << self
+ else
+ ok = orphan
+ end
+ if ok && new_sibling
+ ok = move_above(new_sibling) if self_and_siblings(true).include?(new_sibling)
+ end
+ return ok
+ end
+
+ # orphans the node (sends it to the roots list)
+ # (descendants follow)
+ def orphan
+ self[foreign_key_column] = 0
+ self.send("update")
+ end
+
+ # orphans the node's children
+ # sends all immediate children to the 'roots' list
+ def orphan_children
+ self.class.transaction do
+ children(true).each{|child| child.orphan}
+ end
+ end
+
+ # hands children off to parent
+ # if no parent, children will be orphaned
+ def parent_adopts_children
+ if parent(true)
+ self.class.transaction do
+ children(true).each{|child| parent.children << child}
+ end
+ else
+ orphan_children
+ end
+ end
+
+ # sends self and immediate children to the roots list
+ def orphan_self_and_children
+ self.class.transaction do
+ orphan_children
+ orphan
+ end
+ end
+
+ # hands children off to parent (if possible), then orphans itself
+ def orphan_self_and_parent_adopts_children
+ self.class.transaction do
+ parent_adopts_children
+ orphan
+ end
+ end
+
+ ## List Update Methods
+
+ # moves the item above sibling in the list
+ # defaults to the top of the list
+ def move_above(sibling = nil)
+ if sibling
+ return if (!self_and_siblings(true).include?(sibling) || (sibling == self))
+ if sibling.position_in_list > position_in_list
+ move_to(sibling.position_in_list - 1)
+ else
+ move_to(sibling.position_in_list)
+ end
+ else
+ move_to_top
+ end
+ end
+
+ # move to the top of the list
+ def move_to_top
+ return if position_in_list == 1
+ move_to(1)
+ end
+
+ # swap with the node above self
+ def move_higher
+ return if position_in_list == 1
+ move_to(position_in_list - 1)
+ end
+
+ # swap with the node below self
+ def move_lower
+ return if self == self_and_siblings(true).last
+ move_to(position_in_list + 1)
+ end
+
+ # move to the bottom of the list
+ def move_to_bottom
+ return if self == self_and_siblings(true).last
+ move_to(self_and_siblings.last.position_in_list)
+ end
+
+ ## Destroy Methods
+
+ # sends immediate children to the 'roots' list, then destroy's self
+ def destroy_and_orphan_children
+ self.class.transaction do
+ orphan_children
+ self.destroy
+ end
+ end
+
+ # hands immediate children of to it's parent, then destroy's self
+ def destroy_and_parent_adopts_children
+ self.class.transaction do
+ parent_adopts_children
+ self.destroy
+ end
+ end
+
+ private
+ def find_root
+ node = self
+ node = node.parent while node.parent(true)
+ node
+ end
+
+ def find_ancestors
+ node, nodes = self, []
+ nodes << node = node.parent while node.parent(true)
+ nodes
+ end
+
+ # recursive method
+ def find_descendants(node)
+ @descendants ||= []
+ node.children(true).each do |child|
+ @descendants << child
+ find_descendants(child)
+ end
+ @descendants
+ end
+
+ def add_to_list
+ new_position = position_in_list if (1..self_and_siblings(true).size).include?(position_in_list.to_i)
+ add_to_list_bottom
+ move_to(new_position, true) if new_position
+ end
+
+ def add_to_list_bottom
+ self[order_column] = self_and_siblings.size + 1
+ end
+
+ def move_to(new_position, on_create = false)
+ if parent(true)
+ scope = "#{foreign_key_column} = #{parent.id}"
+ else
+ scope = "#{foreign_key_column} = 0"
+ end
+ if new_position < position_in_list
+ # moving from lower to higher, increment all in between
+ # #{order_column} >= #{new_position} AND #{order_column} < #{position_in_list}
+ self.class.transaction do
+ self.class.update_all(
+ "#{order_column} = (#{order_column} + 1)", "#{scope} AND (#{order_column} BETWEEN #{new_position} AND #{position_in_list - 1})"
+ )
+ if on_create
+ self[order_column] = new_position
+ else
+ update_attribute(order_column, new_position)
+ end
+ end
+ else
+ # moving from higher to lower, decrement all in between
+ # #{order_column} > #{position_in_list} AND #{order_column} <= #{new_position}
+ self.class.transaction do
+ self.class.update_all(
+ "#{order_column} = (#{order_column} - 1)", "#{scope} AND (#{order_column} BETWEEN #{position_in_list + 1} AND #{new_position})"
+ )
+ update_attribute(order_column, new_position)
+ end
+ end
+ end
+
+ def reorder_children
+ self.class.transaction do
+ children(true).each do |child|
+ new_position = children.index(child) + 1
+ child.update_attribute(order_column, new_position) if (child.position_in_list != new_position)
+ end
+ end
+ end
+
+ def reorder_roots
+ self.class.transaction do
+ self.class.roots(true).each do |root|
+ new_position = self.class.roots.index(root) + 1
+ root.update_attribute(order_column, new_position) if (root.position_in_list != new_position)
+ end
+ end
+ end
+
+ protected
+ def destroy_descendants #:nodoc:
+ # before_destroy callback (recursive)
+ @old_parent = self.class.find(self).parent || 'root'
+ self.children(true).each{|child| child.destroy}
+ end
+
+ def check_list_changes #:nodoc:
+ # before_update callback
+ #
+ # Note: to shift to another parent AND specify a position, use shift_to()
+ # i.e. don't assign the object a new position, then new_parent << obj
+ # this will end up at the bottom of the list.
+ #
+ if !self_and_siblings(true).include?(self)
+ add_to_list_bottom
+ @old_parent = self.class.find(self).parent || 'root'
+ end
+ end
+
+ def validate_on_update #:nodoc:
+ if !self_and_siblings(true).include?(self)
+ if self.parent == self
+ errors.add_to_base("cannot be a parent to itself.")
+ elsif (self.parent && self.descendants(true).include?(self.parent))
+ errors.add_to_base("is an ancestor of the new parent.")
+ end
+ end
+ end
+
+ def reorder_old_list #:nodoc:
+ # after_update and after_destroy callback
+ # re-order the old parent's list
+ if @old_parent == 'root'
+ reorder_roots
+ elsif @old_parent
+ @old_parent.reorder_children
+ end
+ end
+
+ #protected
+ #private
+ end #module InstanceMethods
+ end #module OrderedTree
+ end #module Acts
+end #module WizardActsAsOrderedTree
3 lib/person.rb
@@ -0,0 +1,3 @@
+class Person < ActiveRecord::Base
+ acts_as_ordered_tree
+end
15 test/abstract_unit.rb
@@ -0,0 +1,15 @@
+
+ENV["RAILS_ENV"] = "test"
+require File.dirname(__FILE__) + '/../../../../config/environment'
+require 'test_help'
+
+config = YAML::load(IO.read(File.dirname(__FILE__) + '/database.yml'))
+ActiveRecord::Base.logger = Logger.new(File.dirname(__FILE__) + '/debug.log')
+ActiveRecord::Base.establish_connection(config['database'])
+
+require File.dirname(__FILE__) + '/../lib/acts_as_ordered_tree'
+ActiveRecord::Base.send(:include, WizardActsAsOrderedTree::Acts::OrderedTree)
+
+require File.dirname(__FILE__) + '/../lib/person'
+
+load(File.dirname(__FILE__) + '/schema.rb')
413 test/acts_as_ordered_tree_test.rb
@@ -0,0 +1,413 @@
+require File.dirname(__FILE__) + '/abstract_unit'
+class ActsAsOrderedTreeTest < Test::Unit::TestCase
+
+ def setup
+ reload_test_tree
+ end
+
+ def test_validation
+ people = Person.find(:all)
+ # people[0] is gaining a new parent
+ assert !(people[4].children << people[0]),"
+ Validation failed:
+ If you are using the 'validate_on_update' callback, make sure you use 'super'\n"
+ assert_equal "is an ancestor of the new parent.", people[0].errors[:base]
+
+ # people[2] is changing parents
+ assert !(people[7].children << people[2])
+ assert_equal "is an ancestor of the new parent.", people[0].errors[:base]
+
+ assert !(people[3].children << people[3])
+ assert_equal "cannot be a parent to itself.", people[3].errors[:base]
+
+ # remember that the failed operations leave you with tainted objects
+ assert people[2].parent == people[7]
+ people[2].reload
+ people[0].reload
+ assert people[2].parent != people[7]
+ end
+
+ def test_validate_on_update_reloads_descendants
+ people = Person.find(:all)
+ # caching (descendants must be reloaded in validate_on_update)
+ people[2].descendants # load descendants
+ assert people[5].children << people[7]
+ assert people[5].children.include?(people[7])
+ # since people[2].descendants has already been loaded above,
+ # it still includes people[7] as a descendant
+ assert people[2].descendants.include?(people[7])
+ # so, without the reload on people[2].descendants in validate_on_update,
+ # the following would fail
+ assert people[7].children << people[2], 'Validation Failed: descendants must be reloaded in validate_on_update'
+ end
+
+ def test_descendants
+ roots = Person.roots
+ assert !roots.empty?
+ count = 0
+ roots.each{|root| count = count + root.descendants.size + 1}
+ assert count == Person.find(:all).size
+ end
+
+ def test_destroy_descendants
+ people = Person.find(:all)
+ assert_equal 7, people[2].descendants.size + 1
+ assert people[2].destroy
+ assert_equal (people.size - 7), Person.find(:all).size
+ end
+
+ def test_ancestors_and_roots
+ people = Person.find(:all)
+ assert people[7].ancestors == [people[4],people[2],people[0]]
+ assert people[7].root == people[0]
+ assert people[7].class.roots == [people[0],people[11]]
+ end
+
+ def test_destroy_and_reorder_list
+ people = Person.find(:all)
+ assert_equal [people[1],people[2],people[3],people[4],people[7],people[8],people[9],people[10],people[5],people[6]], people[0].descendants
+ assert_equal [people[1],people[2],people[5],people[6]], people[5].self_and_siblings
+ assert_equal 3, people[5].position_in_list
+ # taint people[2].parent (since the plugin protects against this)
+ people[10].children << people[2]
+ assert_equal people[10], people[2].parent
+ assert people[2].destroy
+ assert_equal (people.size - 7), Person.find(:all).size
+ # Note that I don't need to reload self_and_siblings or children in this case,
+ # since the re-ordering action is actually happening against people[0].children
+ # (which is what self_and_syblings returns)
+ assert_equal people[5].self_and_siblings, people[0].children
+ assert_equal [people[1],people[5],people[6]], people[5].self_and_siblings
+ people[5].reload
+ assert_equal 2, people[5].position_in_list
+ # of course, descendants must always be reloaded
+ assert people[0].descendants.include?(people[7])
+ assert !people[0].descendants(true).include?(people[7])
+ end
+
+ def test_reorder_lists
+ people = Person.find(:all)
+ # re-order children
+ assert people[13].children << people[4]
+ assert_equal 5, people[4].position_in_list
+ people[9].reload
+ assert_equal 2, people[9].position_in_list
+ # re-order roots
+ assert people[13].children << people[0]
+ assert_equal 6, people[0].position_in_list
+ people[11].reload
+ assert_equal 1, people[11].position_in_list
+ end
+
+ def test_move_higher
+ people = Person.find(:all)
+ assert people[9].move_higher
+ assert_equal 2, people[9].position_in_list
+ people[4].reload
+ assert_equal 3, people[4].position_in_list
+ end
+
+ def test_move_lower
+ people = Person.find(:all)
+ assert people[4].move_lower
+ assert_equal 3, people[4].position_in_list
+ people[9].reload
+ assert_equal 2, people[9].position_in_list
+ end
+
+ def test_move_to_top
+ people = Person.find(:all)
+ assert people[4].move_to_top
+ people = Person.find(:all)
+ assert_equal 1, people[4].position_in_list
+ assert_equal 2 ,people[3].position_in_list
+ assert_equal 3 ,people[9].position_in_list
+ assert_equal 4, people[10].position_in_list
+ end
+
+ def test_move_to_bottom
+ people = Person.find(:all)
+ assert people[4].move_to_bottom
+ people = Person.find(:all)
+ assert_equal 1, people[3].position_in_list
+ assert_equal 2 ,people[9].position_in_list
+ assert_equal 3 ,people[10].position_in_list
+ assert_equal 4, people[4].position_in_list
+ end
+
+ def test_move_above_moving_higher
+ people = Person.find(:all)
+ assert people[10].move_above(people[4])
+ people = Person.find(:all)
+ assert_equal [people[3],people[10],people[4],people[9]], people[2].children
+ assert_equal 1, people[3].position_in_list
+ assert_equal 2 ,people[10].position_in_list
+ assert_equal 3 ,people[4].position_in_list
+ assert_equal 4, people[9].position_in_list
+ end
+
+ def test_move_above_moving_lower
+ people = Person.find(:all)
+ assert people[3].move_above(people[10])
+ people = Person.find(:all)
+ assert_equal [people[4],people[9],people[3],people[10]], people[2].children
+ assert_equal 1, people[4].position_in_list
+ assert_equal 2 ,people[9].position_in_list
+ assert_equal 3 ,people[3].position_in_list
+ assert_equal 4, people[10].position_in_list
+ end
+
+ def test_shift_to_with_position
+ people = Person.find(:all)
+ assert people[4].shift_to(people[13], people[20])
+ people = Person.find(:all)
+ assert_equal [people[3],people[9],people[10]], people[2].children
+ assert_equal 1, people[3].position_in_list
+ assert_equal 2 ,people[9].position_in_list
+ assert_equal 3 ,people[10].position_in_list
+ assert_equal [people[14],people[15],people[4],people[20],people[21]], people[13].children
+ assert_equal 1, people[14].position_in_list
+ assert_equal 2 ,people[15].position_in_list
+ assert_equal 3 ,people[4].position_in_list
+ assert_equal 4, people[20].position_in_list
+ assert_equal 5, people[21].position_in_list
+ end
+
+ def test_shift_to_without_position
+ people = Person.find(:all)
+ assert people[4].shift_to(people[13])
+ people = Person.find(:all)
+ assert_equal [people[3],people[9],people[10]], people[2].children
+ assert_equal 1, people[3].position_in_list
+ assert_equal 2 ,people[9].position_in_list
+ assert_equal 3 ,people[10].position_in_list
+ assert_equal [people[14],people[15],people[20],people[21],people[4]], people[13].children
+ assert_equal 1, people[14].position_in_list
+ assert_equal 2 ,people[15].position_in_list
+ assert_equal 3 ,people[20].position_in_list
+ assert_equal 4, people[21].position_in_list
+ assert_equal 5, people[4].position_in_list
+ end
+
+ def test_shift_to_roots_without_position__ie__orphan
+ people = Person.find(:all)
+ assert people[4].orphan
+ people = Person.find(:all)
+ assert_equal [people[3],people[9],people[10]], people[2].children
+ assert_equal 1, people[3].position_in_list
+ assert_equal 2 ,people[9].position_in_list
+ assert_equal 3 ,people[10].position_in_list
+ assert_equal [people[0],people[11],people[4]], Person.roots
+ assert_equal 1, people[0].position_in_list
+ assert_equal 2 ,people[11].position_in_list
+ assert_equal 3 ,people[4].position_in_list
+ end
+
+ def test_shift_to_roots_with_position
+ people = Person.find(:all)
+ assert people[4].shift_to(nil, people[11])
+ people = Person.find(:all)
+ assert_equal [people[3],people[9],people[10]], people[2].children
+ assert_equal 1, people[3].position_in_list
+ assert_equal 2 ,people[9].position_in_list
+ assert_equal 3 ,people[10].position_in_list
+ assert_equal [people[0],people[4],people[11]], Person.roots
+ assert_equal 1, people[0].position_in_list
+ assert_equal 2 ,people[4].position_in_list
+ assert_equal 3 ,people[11].position_in_list
+ end
+
+ def test_orphan_children
+ people = Person.find(:all)
+ assert people[2].orphan_children
+ people = Person.find(:all)
+ assert people[2].children.empty?
+ assert_equal [people[0],people[11],people[3],people[4],people[9],people[10]], Person.roots
+ end
+
+ def test_parent_adopts_children
+ people = Person.find(:all)
+ assert people[4].parent_adopts_children
+ people = Person.find(:all)
+ assert people[4].children.empty?
+ assert_equal [people[3],people[4],people[9],people[10],people[7],people[8]], people[2].children
+ end
+
+ def test_orphan_self_and_children
+ people = Person.find(:all)
+ assert people[2].orphan_self_and_children
+ people = Person.find(:all)
+ assert people[2].children.empty?
+ assert_equal [people[0],people[11],people[3],people[4],people[9],people[10],people[2]], Person.roots
+ end
+
+ def test_orphan_self_and_parent_adopts_children
+ people = Person.find(:all)
+ assert people[4].orphan_self_and_parent_adopts_children
+ people = Person.find(:all)
+ assert people[4].children.empty?
+ assert_equal [people[3],people[9],people[10],people[7],people[8]], people[2].children
+ assert_equal 1, people[3].position_in_list
+ assert_equal 2 ,people[9].position_in_list
+ assert_equal 3 ,people[10].position_in_list
+ assert_equal 4, people[7].position_in_list
+ assert_equal 5, people[8].position_in_list
+ assert_equal [people[0],people[11],people[4]], Person.roots
+ end
+
+ def test_destroy_and_orphan_children
+ people = Person.find(:all)
+ assert people[2].destroy_and_orphan_children
+ people = Person.find(:all)
+ # remember, since we deleted people[2], all below get shifted up
+ assert_equal [people[0],people[10],people[2],people[3],people[8],people[9]], Person.roots
+ assert_equal [people[1],people[4],people[5]], people[0].children
+ assert_equal 1, people[1].position_in_list
+ assert_equal 2 ,people[4].position_in_list
+ assert_equal 3 ,people[5].position_in_list
+ end
+
+ def test_destroy_and_parent_adopts_children
+ people = Person.find(:all)
+ assert people[4].destroy_and_parent_adopts_children
+ people = Person.find(:all)
+ # remember, since we deleted people[4], all below get shifted up
+ assert_equal [people[3],people[8],people[9],people[6],people[7]], people[2].children
+ assert_equal 1, people[3].position_in_list
+ assert_equal 2 ,people[8].position_in_list
+ assert_equal 3 ,people[9].position_in_list
+ assert_equal 4, people[6].position_in_list
+ assert_equal 5, people[7].position_in_list
+ end
+
+ def test_create_with_position_method_1
+ people = Person.find(:all)
+ # method 1
+ assert people[2].children << Person.new(:position => 3, :name => 'Person_23')
+ people = Person.find(:all)
+ assert_equal [people[3],people[4],people[22],people[9],people[10]], people[2].children
+ assert_equal 1, people[3].position_in_list
+ assert_equal 2 ,people[4].position_in_list
+ assert_equal 3 ,people[22].position_in_list
+ assert_equal 4, people[9].position_in_list
+ assert_equal 5, people[10].position_in_list
+ end
+
+ def test_create_with_position_method_2
+ people = Person.find(:all)
+ # method 2
+ assert Person.create(:parent_id => people[2].id, :position => 2, :name => 'Person_23')
+ people = Person.find(:all)
+ assert_equal [people[3],people[22],people[4],people[9],people[10]], people[2].children
+ assert_equal 1, people[3].position_in_list
+ assert_equal 2 ,people[22].position_in_list
+ assert_equal 3 ,people[4].position_in_list
+ assert_equal 4, people[9].position_in_list
+ assert_equal 5, people[10].position_in_list
+ end
+
+ def test_create_with_position_method_3
+ people = Person.find(:all)
+ # method 3
+ assert people[2].children.create(:position => 4, :name => 'Person_23')
+ people = Person.find(:all)
+ assert_equal [people[3],people[4],people[9],people[22],people[10]], people[2].children
+ assert_equal 1, people[3].position_in_list
+ assert_equal 2 ,people[4].position_in_list
+ assert_equal 3, people[9].position_in_list
+ assert_equal 4 ,people[22].position_in_list
+ assert_equal 5, people[10].position_in_list
+ end
+
+ def test_create_with_position_method_4
+ people = Person.find(:all)
+ # method 4 (new 'root')
+ assert Person.create(:position => 2, :name => 'Person_23')
+ people = Person.find(:all)
+ assert_equal [people[0],people[22],people[11]], Person.roots
+ assert_equal 1, people[0].position_in_list
+ assert_equal 2 ,people[22].position_in_list
+ assert_equal 3 ,people[11].position_in_list
+ end
+
+ def test_create_with_invalid_position
+ # invalid positions go to bottom of the list
+ people = Person.find(:all)
+ person_23 = people[2].children.create(:position => 15, :name => 'Person_23')
+ assert_equal 5, person_23.position_in_list
+ end
+
+ private
+ # Test Tree
+ #
+ # people[0]
+ # \_ people[1]
+ # \_ people[2]
+ # | \_ people[3]
+ # | \_ people[4]
+ # | | \_ people[7]
+ # | | \_ people[8]
+ # | \_ people[9]
+ # | \_ people[10]
+ # \_ people[5]
+ # \_ people[6]
+ # |
+ # |
+ # people[11]
+ # \_ people[12]
+ # \_ people[13]
+ # | \_ people[14]
+ # | \_ people[15]
+ # | | \_ people[18]
+ # | | \_ people[19]
+ # | \_ people[20]
+ # | \_ people[21]
+ # \_ people[16]
+ # \_ people[17]
+ #
+ #
+ # +----+-----------+----------+-----------+
+ # | id | parent_id | position | name |
+ # +----+-----------+----------+-----------+
+ # | 1 | 0 | 1 | Person_1 |
+ # | 2 | 1 | 1 | Person_2 |
+ # | 3 | 1 | 2 | Person_3 |
+ # | 4 | 3 | 1 | Person_4 |
+ # | 5 | 3 | 2 | Person_5 |
+ # | 6 | 1 | 3 | Person_6 |
+ # | 7 | 1 | 4 | Person_7 |
+ # | 8 | 5 | 1 | Person_8 |
+ # | 9 | 5 | 2 | Person_9 |
+ # | 10 | 3 | 3 | Person_10 |
+ # | 11 | 3 | 4 | Person_11 |
+ # | 12 | 0 | 2 | Person_12 |
+ # | 13 | 12 | 1 | Person_13 |
+ # | 14 | 12 | 2 | Person_14 |
+ # | 15 | 14 | 1 | Person_15 |
+ # | 16 | 14 | 2 | Person_16 |
+ # | 17 | 12 | 3 | Person_17 |
+ # | 18 | 12 | 4 | Person_18 |
+ # | 19 | 16 | 1 | Person_19 |
+ # | 20 | 16 | 2 | Person_20 |
+ # | 21 | 14 | 3 | Person_21 |
+ # | 22 | 14 | 4 | Person_22 |
+ # +----+-----------+----------+-----------+
+ #
+ def reload_test_tree
+ Person.delete_all
+ people = []
+ i = 1
+ people << Person.create(:name => "Person_#{i}")
+ [0,2,0,4,2,-1,11,13,11,15,13].each do |n|
+ if n == -1
+ i = i.next
+ people << Person.create(:name => "Person_#{i}")
+ else
+ 2.times do
+ i = i.next
+ people << people[n].children.create(:name => "Person_#{i}")
+ end
+ end
+ end
+ end
+end
11 test/database.yml
@@ -0,0 +1,11 @@
+
+#database:
+# adapter: mysql
+# database: ordered_tree_test
+# username: root
+# password:
+# host: localhost
+
+database:
+ adapter: sqlite3
+ database: ":memory:"
8 test/schema.rb
@@ -0,0 +1,8 @@
+ActiveRecord::Schema.define(:version => 1) do
+ create_table "people", :force => true do |t|
+ t.column "parent_id" ,:integer ,:null => false ,:default => 0
+ t.column "position" ,:integer
+ t.column "name" ,:string
+ end
+ add_index "people", ["parent_id"], :name => "index_people_on_parent_id"
+end

0 comments on commit 91a7fb6

Please sign in to comment.
Something went wrong with that request. Please try again.